MeshCentral/webserver.js
Josiah Baldwin 150e2337f5
Add options for overriding TLS ciphers used (#5915)
* Add the ability to set TLS cipher suites

Added config option to set the TLS ciphers instead of relying on a hardcoded list of ciphers determined by meshcentral.

* Added option to use default node ciphers

This allows the ciphers used to be set to the recommended ciphers by nodejs, as well as allowing the user to override the ciphers using the "--tls-cipher-list" command line switch for node.

* Updated validArguments array to include "usenodedefaulttlsciphers" and "tlsciphers" as options
2024-03-09 23:45:10 -08:00

9513 lines
642 KiB
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');
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 !== false) { obj.app.use(require('compression')()); }
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;
}
// 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.hostname == 'localhost') && (req.query.domainid != null)) { const d = parent.config.domains[req.query.domainid]; if (d != null) return d; } // This is a localhost access with the domainid specified in the URL
if (req.hostname != null) { const d = obj.dnsDomains[req.hostname.toLowerCase()]; if (d != null) return d; } // If this is a DNS name domain, return it here.
const x = req.url.split('/');
if (x.length < 2) return parent.config.domains[''];
const y = parent.config.domains[x[1].toLowerCase()];
if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
return parent.config.domains[''];
}
function handleLogoutRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
// If a HTTP header is required, check new UserRequiredHttpHeader
if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
res.set({ 'Cache-Control': 'no-store' });
// Destroy the user's session to log them out will be re-created next request
var userid = req.session.userid;
if (req.session.userid) {
var user = obj.users[req.session.userid];
if (user != null) {
obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
}
if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
}
req.session = null;
parent.debug('web', 'handleLogoutRequest: success.');
// If this user was logged in using an authentication strategy and there is a logout URL, use it.
if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
let logouturl = null;
let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
// Setup logout url for oidc
if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
if (typeof domain.authstrategies.oidc.logouturl == 'string') {
logouturl = domain.authstrategies.oidc.logouturl;
} else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + '?post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri;
} else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
}
// Log out all other strategies
} else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
// If custom logout was setup, use it
if (logouturl != null) {
parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
res.redirect(logouturl);
return;
}
}
// This is the default logout redirect to the login page
if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + req.query.key); } else { res.redirect(domain.url + 'login'); }
}
// Return an object with 2FA type if 2-step auth can be skipped
function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
if (parent.config.settings.no2factorauth == true) return null;
// If this login occurred using a login token, no 2FA needed.
if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
// Check if we can skip 2nd factor auth because of the source IP address
if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
}
// Check if a 2nd factor cookie is present
if (typeof req.headers.cookie == 'string') {
const cookies = req.headers.cookie.split('; ');
for (var i in cookies) {
if (cookies[i].startsWith('twofactor=')) {
var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
}
}
}
return null;
}
// Return true if this user has 2-step auth active
function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
// If this login occurred using a login token, no 2FA needed.
if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
// Check if we can skip 2nd factor auth because of the source IP address
if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
}
// Check if a 2nd factor cookie is present
if (typeof req.headers.cookie == 'string') {
const cookies = req.headers.cookie.split('; ');
for (var i in cookies) {
if (cookies[i].startsWith('twofactor=')) {
var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(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));
// See if Messenger 2FA is available
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
// Check if a 2nd factor is present
return ((parent.config.settings.no2factorauth !== true) && (msg2fa || sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.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 = (domain.mailserver != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
var otpsms = (parent.smsserver != null);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
// Check 2FA login cookie
if ((token != null) && (token.startsWith('cookie='))) {
var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
}
// Check email key
if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
var deltaTime = (Date.now() - user.otpekey.d);
if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
user.otpekey = {};
obj.db.SetUser(user);
parent.debug('web', 'checkUserOneTimePassword: success (email).');
func(true, { twoFactorType: 'email' });
return;
}
}
// Check SMS key
if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
var deltaTime = (Date.now() - user.otpsms.d);
if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
delete user.otpsms;
obj.db.SetUser(user);
parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
func(true, { twoFactorType: 'sms' });
return;
}
}
// Check messenger key
if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
var deltaTime = (Date.now() - user.otpmsg.d);
if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
delete user.otpmsg;
obj.db.SetUser(user);
parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
func(true, { twoFactorType: 'messenger' });
return;
}
}
// Check hardware key
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
var authResponse = null;
try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
if ((authResponse != null) && (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 origin
var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
if (httpport != 443) { origin += ':' + httpport; }
var u2fchallenge = null;
if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
var assertionExpectations = {
challenge: u2fchallenge,
origin: origin,
factor: 'either',
fmt: 'fido-u2f',
publicKey: webAuthnKey.publicKey,
prevCounter: webAuthnKey.counter,
userHandle: Buffer.from(user._id, 'binary').toString('base64')
};
var webauthnResponse = null;
try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
// Update the hardware key counter and accept the 2nd factor
webAuthnKey.counter = webauthnResponse.counter;
obj.db.SetUser(user);
parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
func(true, { twoFactorType: 'fido' });
} else {
parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
func(false);
}
return;
}
}
}
}
// Check Google Authenticator
const otplib = require('otplib')
otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
func(true, { twoFactorType: 'otp' });
return;
};
// Check written down keys
if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
var tokenNumber = parseInt(token);
for (var i = 0; i < user.otpkeys.keys.length; i++) {
if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
}
}
}
// Check OTP hardware key (Yubikey OTP)
if ((domain.yubikey != null) && (domain.yubikey.id != null) && (domain.yubikey.secret != null) && (user.otphkeys != null) && (user.otphkeys.length > 0) && (typeof (token) == 'string') && (token.length == 44)) {
var keyId = token.substring(0, 12);
// Find a matching OTP key
var match = false;
for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
// If we have a match, check the OTP
if (match === true) {
var yubikeyotp = require('yubikeyotp');
var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
yubikeyotp.verifyOTP(request, function (err, results) {
if ((results != null) && (results.status == 'OK')) {
parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
func(true, { twoFactorType: 'hwotp' });
} else {
parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
func(false);
}
});
return;
}
}
parent.debug('web', 'checkUserOneTimePassword: fail (2).');
func(false);
}
// Return a U2F hardware key challenge
function getHardwareKeyChallenge(req, domain, user, func) {
var sec = {};
if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
if (user.otphkeys && (user.otphkeys.length > 0)) {
// Get all WebAuthn keys
var webAuthnKeys = [];
for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
if (webAuthnKeys.length > 0) {
// Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
sec.u2f = authnOptions.challenge;
req.session.e = parent.encryptSessionData(sec);
parent.debug('web', 'getHardwareKeyChallenge: success');
func(JSON.stringify(authnOptions));
return;
}
}
// Remove the challenge if present
if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
parent.debug('web', 'getHardwareKeyChallenge: fail');
func('');
}
// Redirect a root request to a different page
function handleRootRedirect(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
res.redirect(domain.rootredirect + getQueryPortion(req));
}
function handleLoginRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
if (req.session == null) { req.session = {}; }
// Check if this is a banned ip address
if (obj.checkAllowLogin(req) == false) {
// Wait and redirect the user
setTimeout(function () {
req.session.messageid = 114; // IP address blocked, try again later.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
return;
}
// Normally, use the body username/password. If this is a token, use the username/password in the session.
var xusername = req.body.username, xpassword = req.body.password;
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
const sec = parent.decryptSessionData(req.session.e);
xusername = sec.tuser; xpassword = sec.tpass;
}
// Authenticate the user
obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
if (userid) {
var user = obj.users[userid];
// Check if we are in maintenance mode
if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
req.session.messageid = 115; // Server under maintenance
req.session.loginmode = 1;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
var push2fa = ((parent.firebase != null) && (user.otpdev != null));
// Check if two factor can be skipped
const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
// Check if this user has 2-step login active
if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
if ((req.body.hwtoken == '**timeout**')) {
delete req.session; // Clear the session
res.redirect(domain.url + getQueryPortion(req));
return;
}
if ((req.body.hwtoken == '**email**') && email2fa) {
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA email to: ' + user.email);
domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
req.session.messageid = 2; // "Email sent" message
req.session.loginmode = 4;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
if ((req.body.hwtoken == '**sms**') && sms2fa) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
req.session.messageid = 4; // "SMS sent" message
req.session.loginmode = 4;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
if ((req.body.hwtoken == '**msg**') && msg2fa) {
// Cause a token to be sent to the user's messenger account
user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm message was sent
req.session.messageid = 6; // "Message sent" message
req.session.loginmode = 4;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Handle device push notification 2FA request
// We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const sessioncode = obj.crypto.randomBytes(24).toString('base64');
// Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
// Get the HTTPS port
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
// Get the agent connection server name
var serverName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
// Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
// Request that the login page wait for device auth
req.session.messageid = 5; // "Sending notification..." message
req.session.passhint = url;
req.session.loginmode = 8;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
if (result == false) {
var randomWaitTime = 0;
// Check if 2FA is allowed for this IP address
if (obj.checkAllow2Fa(req) == false) {
// Wait and redirect the user
setTimeout(function () {
req.session.messageid = 114; // IP address blocked, try again later.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
return;
}
// 2-step auth is required, but the token is not present or not valid.
if ((req.body.token != null) || (req.body.hwtoken != null)) {
randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
req.session.messageid = 108; // Invalid token, try again.
obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.port, { useragent: req.headers['user-agent'] });
parent.debug('web', 'handleLoginRequest: invalid 2FA token');
const ua = obj.getUserAgentInfo(req);
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
obj.setbad2Fa(req);
} else {
parent.debug('web', 'handleLoginRequest: 2FA token required');
}
// Wait and redirect the user
setTimeout(function () {
req.session.loginmode = 4;
if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; }
if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, randomWaitTime);
} else {
// Check if we need to remember this device
if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
var maxCookieAge = domain.twofactorcookiedurationdays;
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
}
// Check if email address needs to be confirmed
const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
if (emailcheck && (user.emailVerified !== true)) {
parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
req.session.messageid = 3; // "Email verification required" message
req.session.loginmode = 7;
req.session.passhint = user.email;
req.session.cuserid = userid;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Login successful
parent.debug('web', 'handleLoginRequest: successful 2FA login');
if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
}
});
return;
}
// Check if email address needs to be confirmed
const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
if (emailcheck && (user.emailVerified !== true)) {
parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
req.session.messageid = 3; // "Email verification required" message
req.session.loginmode = 7;
req.session.passhint = user.email;
req.session.cuserid = userid;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Login successful
parent.debug('web', 'handleLoginRequest: successful login');
if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
} else {
// Login failed, log the error
obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
// Wait a random delay
setTimeout(function () {
// If the account is locked, display that.
if (typeof xusername == 'string') {
var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
if (err == 'locked') {
parent.debug('web', 'handleLoginRequest: login failed, locked account');
req.session.messageid = 110; // Account locked.
const ua = obj.getUserAgentInfo(req);
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + req.clientIp, msgid: 109, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
obj.setbadLogin(req);
} else if (err == 'denied') {
parent.debug('web', 'handleLoginRequest: login failed, access denied');
req.session.messageid = 111; // Access denied.
const ua = obj.getUserAgentInfo(req);
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Denied user login from ' + req.clientIp, msgid: 155, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
obj.setbadLogin(req);
} else {
parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
req.session.messageid = 112; // Login failed, check username and password.
const ua = obj.getUserAgentInfo(req);
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp, msgid: 110, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
obj.setbadLogin(req);
}
}
// Clean up login mode and display password hint if present.
delete req.session.loginmode;
if ((passhint != null) && (passhint.length > 0)) {
req.session.passhint = passhint;
} else {
delete req.session.passhint;
}
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
}
});
}
function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
// Check if we need to change the password
if ((typeof user.passchange == 'number') && ((user.passchange == -1) || ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.reset == 'number') && (user.passchange + (domain.passwordrequirements.reset * 86400) < Math.floor(Date.now() / 1000))))) {
// Request a password change
parent.debug('web', 'handleLoginRequest: login ok, password change requested');
req.session.loginmode = 6;
req.session.messageid = 113; // Password change requested.
// Decrypt any session data
const sec = parent.decryptSessionData(req.session.e);
sec.rtuser = xusername;
sec.rtpass = xpassword;
req.session.e = parent.encryptSessionData(sec);
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Save login time
user.pastlogin = user.login;
user.login = user.access = Math.floor(Date.now() / 1000);
obj.db.SetUser(user);
// Notify account login
const targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
const ua = obj.getUserAgentInfo(req);
const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login from ' + req.clientIp + ', ' + ua.browserStr + ', ' + ua.osStr, domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], rport: req.connection.remotePort };
if (loginOptions != null) {
if ((loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { loginEvent.tokenName = loginOptions.tokenName; loginEvent.tokenUser = loginOptions.tokenUser; } // If a login token was used, add it to the event.
if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
}
obj.parent.DispatchEvent(targets, obj, loginEvent);
// Regenerate session when signing in to prevent fixation
//req.session.regenerate(function () {
// Store the user's primary key in the session store to be retrieved, or in this case the entire user object
delete req.session.e;
delete req.session.u2f;
delete req.session.loginmode;
delete req.session.tuserid;
delete req.session.tuser;
delete req.session.tpass;
delete req.session.temail;
delete req.session.tsms;
delete req.session.tmsg;
delete req.session.tpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
delete req.session.expire;
delete req.session.currentNode;
req.session.userid = userid;
req.session.ip = req.clientIp;
setSessionRandom(req);
obj.parent.authLog('https', 'Accepted password for ' + (xusername ? xusername : userid) + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
// If a login token was used, add this information and expire time to the session.
if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
req.session.loginToken = loginOptions.tokenUser;
if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
}
if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
if (req.body.host) {
// TODO: This is a terrible search!!! FIX THIS.
/*
obj.db.GetAllType('node', function (err, docs) {
for (var i = 0; i < docs.length; i++) {
if (docs[i].name == req.body.host) {
req.session.currentNode = docs[i]._id;
break;
}
}
console.log("CurrentNode: " + req.session.currentNode);
// This redirect happens after finding node is completed
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
});
*/
parent.debug('web', 'handleLoginRequest: login ok (1)');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
} else {
parent.debug('web', 'handleLoginRequest: login ok (2)');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
//});
}
function handleCreateAccountRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
// Check if we are in maintenance mode
if (parent.config.settings.maintenancemode != null) {
req.session.messageid = 115; // Server under maintenance
req.session.loginmode = 1;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Always lowercase the email address
if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
// If the email is the username, set this here.
if (domain.usernameisemail) { req.body.username = req.body.email; }
// Check if there is domain.newAccountToken, check if supplied token is valid
if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
req.session.loginmode = 2;
req.session.messageid = 103; // Invalid account creation token.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// If needed, check the new account creation CAPTCHA
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
req.session.loginmode = 2;
req.session.messageid = 117; // Invalid security check
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
}
// Accounts that start with ~ are not allowed
if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
req.session.loginmode = 2;
req.session.messageid = 100; // Unable to create account.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Count the number of users in this domain
var domainUserCount = 0;
for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
// Check if we are allowed to create new users using the login screen
if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
res.sendStatus(401);
return;
}
// Check if this request is for an allows email domain
if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
var i = -1;
if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
if (i == -1) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
req.session.loginmode = 2;
req.session.messageid = 100; // Unable to create account.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
if (emailok == false) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
req.session.loginmode = 2;
req.session.messageid = 100; // Unable to create account.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
}
// Check if we exceed the maximum number of user accounts
obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
if (maxExceed) {
parent.debug('web', 'handleCreateAccountRequest: account limit reached');
req.session.loginmode = 2;
req.session.messageid = 101; // Account limit reached.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
if (!obj.common.validateUsername(req.body.username, 1, 64) || !obj.common.validateEmail(req.body.email, 1, 256) || !obj.common.validateString(req.body.password1, 1, 256) || !obj.common.validateString(req.body.password2, 1, 256) || (req.body.password1 != req.body.password2) || req.body.username == '~' || !obj.common.checkPasswordRequirements(req.body.password1, domain.passwordrequirements)) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
req.session.loginmode = 2;
req.session.messageid = 100; // Unable to create account.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Check if this email was already verified
obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
if ((docs != null) && (docs.length > 0)) {
parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
req.session.loginmode = 2;
req.session.messageid = 102; // Existing account with this email address.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Check if user exists
if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
parent.debug('web', 'handleCreateAccountRequest: Username already exists');
req.session.loginmode = 2;
req.session.messageid = 104; // Username already exists.
} else {
var user = { type: 'user', _id: 'user/' + domain.id + '/' + req.body.username.toLowerCase(), name: req.body.username, email: req.body.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) { hint = hint.substring(0, 250); } user.passhint = hint; }
if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
// Auto-join any user groups
if (typeof domain.newaccountsusergroups == 'object') {
for (var i in domain.newaccountsusergroups) {
var ugrpid = domain.newaccountsusergroups[i];
if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
var ugroup = obj.userGroups[ugrpid];
if (ugroup != null) {
// Add group to the user
if (user.links == null) { user.links = {}; }
user.links[ugroup._id] = { rights: 1 };
// Add user to the group
ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
db.Set(ugroup);
// Notify user group change
var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
}
}
}
obj.users[user._id] = user;
req.session.userid = user._id;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
// Create a user, generate a salt and hash the password
require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
if (err) throw err;
user.salt = salt;
user.hash = hash;
delete user.passtype;
obj.db.SetUser(user);
// Send the verification email
if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); }
}, 0);
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id };
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
}
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
});
}
}
});
}
// Called to process an account password reset
function handleResetPasswordRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
// Decrypt any session data
const sec = parent.decryptSessionData(req.session.e);
// Check everything is ok
const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
if ((allowAccountReset === false) || (domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof sec.rtuser != 'string') || (typeof sec.rtpass != 'string')) {
parent.debug('web', 'handleResetPasswordRequest: checks failed');
delete req.session.e;
delete req.session.u2f;
delete req.session.loginmode;
delete req.session.tuserid;
delete req.session.tuser;
delete req.session.tpass;
delete req.session.temail;
delete req.session.tsms;
delete req.session.tmsg;
delete req.session.tpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Authenticate the user
obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
if (userid) {
// Login
var user = obj.users[userid];
// If we have password requirements, check this here.
if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
req.session.loginmode = 6;
req.session.messageid = 105; // Password rejected, use a different one.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Check if the password is the same as a previous one
obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
if (result != 0) {
// This is the same password as an older one, request a password change again
parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
req.session.loginmode = 6;
req.session.messageid = 105; // Password rejected, use a different one.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Update the password, use a different salt.
require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
const nowSeconds = Math.floor(Date.now() / 1000);
if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
if (domain.passwordrequirements != null) {
// Save password hint if this feature is enabled
if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
// Save previous password if this feature is enabled
if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
if (user.oldpasswords == null) { user.oldpasswords = []; }
user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
}
}
user.salt = salt;
user.hash = hash;
user.passchange = user.access = nowSeconds;
delete user.passtype;
obj.db.SetUser(user);
// Event the account change
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
// Login successful
parent.debug('web', 'handleResetPasswordRequest: success');
req.session.userid = userid;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
const sec = parent.decryptSessionData(req.session.e);
completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
}, 0);
}
}, 0);
} else {
// Failed, error out.
parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
delete req.session.e;
delete req.session.u2f;
delete req.session.loginmode;
delete req.session.tuserid;
delete req.session.tuser;
delete req.session.tpass;
delete req.session.temail;
delete req.session.tsms;
delete req.session.tmsg;
delete req.session.tpush;
delete req.session.messageid;
delete req.session.passhint;
delete req.session.cuserid;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
});
}
// Called to process an account reset request
function handleResetAccountRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
if ((allowAccountReset === false) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (obj.args.lanonly == true) || (obj.parent.certificates.CommonName == null) || (obj.parent.certificates.CommonName.indexOf('.') == -1)) { parent.debug('web', 'handleResetAccountRequest: check failed'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
// Always lowercase the email address
if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
// Get the email from the body or session.
var email = req.body.email;
if ((email == null) || (email == '')) { email = req.session.temail; }
// Check the email string format
if (!email || checkEmail(email) == false) {
parent.debug('web', 'handleResetAccountRequest: Invalid email');
req.session.loginmode = 3;
req.session.messageid = 106; // Invalid email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
// Remove all accounts that start with ~ since they are special accounts.
var cleanDocs = [];
if ((err == null) && (docs.length > 0)) {
for (var i in docs) {
const user = docs[i];
const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
}
}
docs = cleanDocs;
// Check if we have any account that match this email address
if ((err != null) || (docs.length == 0)) {
parent.debug('web', 'handleResetAccountRequest: Account not found');
req.session.loginmode = 3;
req.session.messageid = 1; // If valid, reset mail sent. Instead of "Account not found" (107), we send this hold on message so users can't know if this account exists or not.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// If many accounts have the same validated e-mail, we are going to use the first one for display, but sent a reset email for all accounts.
var responseSent = false;
for (var i in docs) {
var user = docs[i];
if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
// Second factor setup, request it now.
checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
if (result == false) {
if (i == 0) {
// Check if 2FA is allowed for this IP address
if (obj.checkAllow2Fa(req) == false) {
// Wait and redirect the user
setTimeout(function () {
req.session.messageid = 114; // IP address blocked, try again later.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
return;
}
// 2-step auth is required, but the token is not present or not valid.
parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
if ((req.body.token != null) || (req.body.hwtoken != null)) {
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
if ((req.body.hwtoken == '**sms**') && sms2fa) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
req.session.messageid = 4; // SMS sent.
} else if ((req.body.hwtoken == '**msg**') && msg2fa) {
// Cause a token to be sent to the user's messager account
user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
req.session.messageid = 6; // Message sent.
} else {
req.session.messageid = 108; // Invalid token, try again.
const ua = obj.getUserAgentInfo(req);
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
obj.setbad2Fa(req);
}
}
req.session.loginmode = 5;
req.session.temail = email;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
} else {
// Send email to perform recovery.
delete req.session.temail;
if (domain.mailserver != null) {
domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
if (i == 0) {
parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
req.session.loginmode = 1;
req.session.messageid = 1; // If valid, reset mail sent.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
} else {
if (i == 0) {
parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
req.session.loginmode = 3;
req.session.messageid = 109; // Unable to sent email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
}
}
});
} else {
// No second factor, send email to perform recovery.
if (domain.mailserver != null) {
domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
if (i == 0) {
parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
req.session.loginmode = 1;
req.session.messageid = 1; // If valid, reset mail sent.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
} else {
if (i == 0) {
parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
req.session.loginmode = 3;
req.session.messageid = 109; // Unable to sent email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
}
}
}
}
});
}
}
// Handle account email change and email verification request
function handleCheckAccountEmailRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.mailserver == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.session.cuserid != 'string') || (obj.users[req.session.cuserid] == null) || (!obj.common.validateEmail(req.body.email, 1, 256))) { parent.debug('web', 'handleCheckAccountEmailRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
// Always lowercase the email address
if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
// Get the email from the body or session.
var email = req.body.email;
if ((email == null) || (email == '')) { email = req.session.temail; }
// Check if this request is for an allows email domain
if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
var i = -1;
if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
if (i == -1) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
req.session.loginmode = 7;
req.session.messageid = 106; // Invalid email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
if (emailok == false) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
req.session.loginmode = 7;
req.session.messageid = 106; // Invalid email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
}
// Check the email string format
if (!email || checkEmail(email) == false) {
parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
req.session.loginmode = 7;
req.session.messageid = 106; // Invalid email.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Check is email already exists
obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
// Email already exists
req.session.messageid = 102; // Existing account with this email address.
} else {
// Update the user and notify of user email address change
var user = obj.users[req.session.cuserid];
if (user.email != email) {
user.email = email;
db.SetUser(user);
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
parent.DispatchEvent(targets, obj, event);
}
// Send the verification email
domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
// Send the response
req.session.messageid = 2; // Email sent.
}
req.session.loginmode = 7;
delete req.session.cuserid;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
});
}
}
// Called to process a web based email verification request
function handleCheckMailRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.query.c != null) {
var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
var idsplit = cookie.u.split('/');
if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 1, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
} else {
obj.db.Get(cookie.u, function (err, docs) {
if (docs.length == 0) {
parent.debug('web', 'handleCheckMailRequest: Invalid username.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 2, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(idsplit[1]).replace(/'/g, '%27') }, req, domain));
} else {
var user = docs[0];
if (user.email != cookie.e) {
parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 3, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
} else {
if (cookie.a == 1) {
// Account email verification
if (user.emailVerified == true) {
parent.debug('web', 'handleCheckMailRequest: email already verified.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 4, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
} else {
obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
parent.debug('web', 'handleCheckMailRequest: email already in use.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 5, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27') }, req, domain));
} else {
parent.debug('web', 'handleCheckMailRequest: email verification success.');
// Set the verified flag
obj.users[user._id].emailVerified = true;
user.emailVerified = true;
obj.db.SetUser(user);
// Event the change
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(user.email) + ')', domain: domain.id };
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
// Send the confirmation page
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 6, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
// Send a notification
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
// Send to authLog
obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
}
});
}
} else if (cookie.a == 2) {
// Account reset
if (user.emailVerified != true) {
parent.debug('web', 'handleCheckMailRequest: email not verified.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 7, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.email), arg2: EscapeHtml(user.name) }, req, domain));
} else {
if (req.query.confirm == 1) {
// Set a temporary password
obj.crypto.randomBytes(16, function (err, buf) {
var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
require('./pass').hash(newpass, function (err, salt, hash, tag) {
if (err) throw err;
// Change the password
var userinfo = obj.users[user._id];
userinfo.salt = salt;
userinfo.hash = hash;
delete userinfo.passtype;
userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
delete userinfo.passhint;
obj.db.SetUser(userinfo);
// Event the change
var event = { etype: 'user', userid: user._id, username: userinfo.name, account: obj.CloneSafeUser(userinfo), action: 'accountchange', msg: 'Password reset for user ' + EscapeHtml(user.name), domain: domain.id };
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
// Send the new password
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 8, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.name), arg2: EscapeHtml(newpass) }, req, domain));
parent.debug('web', 'handleCheckMailRequest: send temporary password.');
// Send to authLog
obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
}, 0);
});
} else {
// Display a link for the user to confirm password reset
// We must do this because GMail will also load this URL a few seconds after the user does and we don't want to cause two password resets.
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 14, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
}
}
} else {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 9, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
}
}
}
});
}
} else {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 10, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
}
}
}
// Called to process an agent invite GET/POST request
function handleInviteRequest(req, res) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((req.body == null) || (req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
// Each for a device group that has this invite code.
for (var i in obj.meshes) {
if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].deleted == null) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) {
// Send invitation link, valid for 1 minute.
res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + req.query.key) : '') + (req.query.hide ? ('&hide=' + req.query.hide) : ''));
return;
}
}
render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
}
// Called to render the MSTSC (RDP) or SSH web page
function handleMSTSCRequest(req, res, page) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
// Check if we are in maintenance mode
if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
return;
}
// Set features we want to send to this page
var features = 0;
if (domain.allowsavingdevicecredentials === false) { features |= 1; }
if (req.query.ws != null) {
// This is a query with a websocket relay cookie, check that the cookie is valid and use it.
var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
// Fetch the node from the database
obj.db.Get(rcookie.nodeid, function (err, nodes) {
if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
const node = nodes[0];
// Check if we have SSH/RDP credentials for this device
var serverCredentials = 0;
if (domain.allowsavingdevicecredentials !== false) {
if (page == 'ssh') {
if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
} else {
if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password
}
}
// Render the page
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
});
return;
}
}
// Get the logged in user if present
var user = null;
// If there is a login token, use that
if (req.query.login != null) {
var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
}
// If no token, see if we have an active session
if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
// If still no user, see if we have a default user
if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
// No user login, exit now
if (user == null) { res.sendStatus(401); return; }
// Check the nodeid
if (req.query.node != null) {
var nodeidsplit = req.query.node.split('/');
if (nodeidsplit.length == 1) {
req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
} else if (nodeidsplit.length == 3) {
if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
} else {
req.query.node = null; // Bad nodeid
}
}
// If there is no nodeid, exit now
if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
// Fetch the node from the database
obj.db.Get(req.query.node, function (err, nodes) {
if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
const node = nodes[0];
// Check access rights, must have remote control rights
if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
// Figure out the target port
var port = 0, serverCredentials = false;
if (page == 'ssh') {
// SSH port
port = 22;
if (typeof node.sshport == 'number') { port = node.sshport; }
// Check if we have SSH credentials for this device
if (domain.allowsavingdevicecredentials !== false) {
if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
}
} else {
// RDP port
port = 3389;
if (typeof node.rdpport == 'number') { port = node.rdpport; }
// Check if we have RDP credentials for this device
if (domain.allowsavingdevicecredentials !== false) {
if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password
}
}
if (req.query.port != null) { var qport = 0; try { qport = parseInt(req.query.port); } catch (ex) { } if ((typeof qport == 'number') && (qport > 0) && (qport < 65536)) { port = qport; } }
// Generate a cookie and respond
var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
});
}
// Called to handle push-only requests
function handleFirebasePushOnlyRelayRequest(req, res) {
parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
var data = null;
try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
if (typeof data != 'object') { res.sendStatus(404); return; }
if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
if (typeof data.payload != 'object') { res.sendStatus(404); return; }
if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
if (typeof data.options != 'object') { res.sendStatus(404); return; }
if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
});
}
// Called to handle two-way push notification relay request
function handleFirebaseRelayRequest(ws, req) {
parent.debug('email', 'handleFirebaseRelayRequest');
if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; }
obj.parent.firebase.setupRelay(ws);
}
// Called to process an agent invite request
function handleAgentInviteRequest(req, res) {
const domain = getDomain(req);
if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.query.c != null) {
// A cookie is specified in the query string, use that
var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
if (cookie == null) { res.sendStatus(404); return; }
var mesh = obj.meshes[cookie.mid];
if (mesh == null) { res.sendStatus(404); return; }
var installflags = cookie.f;
if (typeof installflags != 'number') { installflags = 0; }
var showagents = cookie.ag;
if (typeof showagents != 'number') { showagents = 0; }
parent.debug('web', 'handleAgentInviteRequest using cookie.');
// Build the mobile agent URL, this is used to connect mobile devices
var agentServerName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
var xdomain = (domain.dns == null) ? domain.id : '';
var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags, showagents: showagents, magenturl: magenturl, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
} else if (req.query.m != null) {
// The MeshId is specified in the query string, use that
var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
if (mesh == null) { res.sendStatus(404); return; }
var installflags = 0;
if (req.query.f) { installflags = parseInt(req.query.f); }
if (typeof installflags != 'number') { installflags = 0; }
var showagents = 0;
if (req.query.f) { showagents = parseInt(req.query.ag); }
if (typeof showagents != 'number') { showagents = 0; }
parent.debug('web', 'handleAgentInviteRequest using meshid.');
// Build the mobile agent URL, this is used to connect mobile devices
var agentServerName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
var xdomain = (domain.dns == null) ? domain.id : '';
var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags, showagents: showagents, magenturl: magenturl, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
}
}
// Called to process an agent invite request
function handleUserImageRequest(req, res) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
var imageUserId = req.session.userid;
if ((req.query.id != null)) {
var user = obj.users[req.session.userid];
if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
imageUserId = 'user/' + domain.id + '/' + req.query.id;
}
obj.db.Get('im' + imageUserId, function (err, docs) {
if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
var imagebase64 = docs[0].image;
if (imagebase64.startsWith('data:image/png;base64,')) {
res.set('Content-Type', 'image/png');
res.set({ 'Cache-Control': 'no-store' });
res.send(Buffer.from(imagebase64.substring(22), 'base64'));
} else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
res.set('Content-Type', 'image/jpeg');
res.set({ 'Cache-Control': 'no-store' });
res.send(Buffer.from(imagebase64.substring(23), 'base64'));
} else {
res.sendStatus(404);
}
});
}
function handleDeleteAccountRequest(req, res, direct) {
parent.debug('web', 'handleDeleteAccountRequest()');
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
var user = null;
if (req.body.authcookie) {
// If a authentication cookie is provided, decode it here
var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
} else {
// Check if the user is logged and we have all required parameters
if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
} else {
user = obj.users[req.session.userid];
}
}
if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
// Check if the password is correct
obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
var deluser = obj.users[userid];
if ((userid != null) && (deluser != null)) {
// Remove all links to this user
if (deluser.links != null) {
for (var i in deluser.links) {
if (i.startsWith('mesh/')) {
// Get the device group
var mesh = obj.meshes[i];
if (mesh) {
// Remove user from the mesh
if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
// Notify mesh change
var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id, invite: mesh.invite };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
}
} else if (i.startsWith('node/')) {
// Get the node and the rights for this node
obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
// Remove the link and save the node to the database
delete node.links[deluser._id];
if (Object.keys(node.links).length == 0) { delete node.links; }
db.Set(obj.cleanDevice(node));
// Event the node change
var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: ('Removed user device rights for ' + node.name), node: obj.CloneSafeNode(node) }
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
});
} else if (i.startsWith('ugrp/')) {
// Get the device group
var ugroup = obj.userGroups[i];
if (ugroup) {
// Remove user from the user group
if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
// Notify user group change
var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Removed user ' + deluser.name + ' from user group ' + ugroup.name, addUserDomain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
}
}
}
}
obj.db.Remove('ws' + deluser._id); // Remove user web state
obj.db.Remove('nt' + deluser._id); // Remove notes for this user
obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
obj.db.Remove('im' + deluser._id); // Remove image for this user
// Delete any login tokens
parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
});
// Delete all files on the server for this account
try {
var deluserpath = obj.getServerRootFilePath(deluser);
if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
} catch (e) { }
// Remove the user
obj.db.Remove(deluser._id);
delete obj.users[deluser._id];
req.session = null;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
parent.debug('web', 'handleDeleteAccountRequest: removed user.');
} else {
parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
}
});
}
// Check a user's password
obj.checkUserPassword = function (domain, user, password, func) {
// Check the old password
if (user.passtype != null) {
// IIS default clear or weak password hashing (SHA-1)
require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
if (hash == user.hash) {
if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
parent.debug('web', 'checkUserPassword: SHA-1 ok.');
return func(true); // Allow password change
}
func(false);
});
} else {
// Default strong password hashing (pbkdf2 SHA384)
require('./pass').hash(password, user.salt, function (err, hash, tag) {
if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
if (hash == user.hash) {
if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
return func(true); // Allow password change
}
func(false);
}, 0);
}
}
// Check a user's old passwords
// Callback: 0=OK, 1=OldPass, 2=CommonPass
obj.checkOldUserPasswords = function (domain, user, password, func) {
// Check how many old passwords we need to check
if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
if (user.oldpasswords != null) {
const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
}
} else {
delete user.oldpasswords;
}
// If there is no old passwords, exit now.
var oldPassCount = 1;
if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
// Test against common passwords if this feature is enabled
// Example of common passwords: 123456789, password123
if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
oldPassCheckState.count++;
require('wildleek')(password).then(function (wild) {
if (wild == true) { oldPassCheckState.response = 2; }
if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
});
}
// Try current password
require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
if (--tag.count == 0) { tag.func(tag.response); }
}, oldPassCheckState);
// Try each old password
if (user.oldpasswords != null) {
for (var i in user.oldpasswords) {
const oldpassword = user.oldpasswords[i];
// Default strong password hashing (pbkdf2 SHA384)
require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
}, { oldPassword: oldpassword, state: oldPassCheckState });
}
}
}
// Handle password changes
function handlePasswordChangeRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
// Check if the user is logged and we have all required parameters
if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Get the current user
var user = obj.users[req.session.userid];
if (!user) {
parent.debug('web', 'handlePasswordChangeRequest: user not found.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Check account settings locked
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
return;
}
// Check old password
obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
if (result == true) {
// Check if the new password is allowed, only do this if this feature is enabled.
parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
if (result == 1) {
parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else if (result == 2) {
parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
// Update the password
require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
const nowSeconds = Math.floor(Date.now() / 1000);
if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
if (domain.passwordrequirements != null) {
// Save password hint if this feature is enabled
if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
// Save previous password if this feature is enabled
if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
if (user.oldpasswords == null) { user.oldpasswords = []; }
user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
}
}
user.salt = salt;
user.hash = hash;
user.passchange = user.access = nowSeconds;
delete user.passtype;
obj.db.SetUser(user);
req.session.viewmode = 2;
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id });
}, 0);
}
});
}
});
}
// Called when a strategy login occurred
// This is called after a successful Oauth to Twitter, Google, GitHub...
function handleStrategyLogin(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
const strategy = domain.authstrategies[req.user.strategy];
const groups = { 'enabled': typeof strategy.groups == 'object' }
parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
if (groups.enabled) { // Groups only available for OIDC strategy currently
groups.userMemberships = obj.common.convertStrArray(req.user.groups)
groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false
groups.syncMemberships = []
groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false
groups.grantAdmin = false
groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true
groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required)
groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin)
groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter)
// Fancy Logs
let groupMessage = ''
if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
// Check user membership in required groups
if (groups.requiredGroups != null) {
let match = false
for (var i in groups.requiredGroups) {
if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
match = true;
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
}
}
if (match === false) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No memberhip to required group.`);
req.session.loginmode = 1;
req.session.messageid = 111; // Access Denied.
res.redirect(domain.url + getQueryPortion(req));
return;
}
}
// Check user membership in admin groups
if (groups.siteAdminEnabled === true) {
groups.grantAdmin = false;
for (var i in strategy.groups.siteadmin) {
if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
groups.siteAdmin = strategy.groups.siteadmin[i];
groups.grantAdmin = true;
break;
}
}
}
// Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
if (groups.syncEnabled === true) {
for (var i in groups.syncFilter) {
if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
}
if (groups.syncMemberships.length > 0) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Filtered user memberships from config to sync: ${groups.syncMemberships.join(', ')}`);
} else {
groups.syncMemberships = null;
groups.syncEnabled = false
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found after filter: ${strategy.groups.sync.filter.join(', ')}`);
}
}
}
// Check if the user already exists
const userid = 'user/' + domain.id + '/' + req.user.sid;
var user = obj.users[userid];
if (user == null) {
var newAccountAllowed = false;
var newAccountRealms = null;
if (domain.newaccounts === true) { newAccountAllowed = true; }
if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
if (domain.authstrategies[req.user.strategy]) {
if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
}
if (newAccountAllowed === true) {
// Create the user
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
if (domain.authstrategies[req.user.strategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
obj.users[userid] = user;
// Auto-join any user groups
var newaccountsusergroups = null;
if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
if (newaccountsusergroups) {
for (var i in newaccountsusergroups) {
var ugrpid = newaccountsusergroups[i];
if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
var ugroup = obj.userGroups[ugrpid];
if (ugroup != null) {
// Add group to the user
if (user.links == null) { user.links = {}; }
user.links[ugroup._id] = { rights: 1 };
// Add user to the group
ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
db.Set(ugroup);
// Notify user group change
var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
}
}
}
if (groups.enabled === true) {
// Sync the user groups if enabled
if (groups.syncEnabled === true) {
// Set groupType to the preset name if it exists, otherwise use the strategy name
const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
}
// See if the user is a member of the site admin group.
if (groups.grantAdmin === true) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
user.siteadmin = 0xFFFFFFFF;
}
}
// Save the user
obj.db.SetUser(user);
// Event user creation
var targets = ['*', 'server-users'];
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, username is ' + user.name, domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
parent.DispatchEvent(targets, obj, event);
req.session.userid = userid;
setSessionRandom(req);
// Notify account login using SSO
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
const ua = obj.getUserAgentInfo(req);
const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
obj.parent.DispatchEvent(targets, obj, loginEvent);
} else {
// New users not allowed
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
req.session.loginmode = 1;
req.session.messageid = 100; // Unable to create account.
res.redirect(domain.url + getQueryPortion(req));
return;
}
} else { // Login success
// Check for basic changes
var userChanged = false;
if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
if (groups.enabled === true) {
// Sync the user groups if enabled
if (groups.syncEnabled === true) {
syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
}
// See if the user is a member of the site admin group.
if (groups.siteAdminEnabled === true) {
if (groups.grantAdmin === true) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
} else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
delete user.siteadmin;
userChanged = true;
}
}
}
// Update db record for user if there are changes detected
if (userChanged) {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
obj.db.SetUser(user);
// Event user change
var targets = ['*', 'server-users'];
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
parent.DispatchEvent(targets, obj, event);
}
req.session.userid = userid;
setSessionRandom(req);
// Notify account login using SSO
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
const ua = obj.getUserAgentInfo(req);
const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
obj.parent.DispatchEvent(targets, obj, loginEvent);
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
}
} else {
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" REQUEST CONTAINS NO USER OR SID`);
}
parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: User Authenticated: ${JSON.stringify(user)}`);
//res.redirect(domain.url); // This does not handle cookie correctly.
res.set('Content-Type', 'text/html');
res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
}
// Indicates that any request to "/" should render "default" or "login" depending on login state
function handleRootRequest(req, res, direct) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
// If a HTTP header is required, check new UserRequiredHttpHeader
if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
// If the session is expired, clear it.
if ((req.session != null) && (typeof req.session.expire == 'number') && ((req.session.expire - Date.now()) <= 0)) { for (var i in req.session) { delete req.session[i]; } }
// Check if we are in maintenance mode
if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
parent.debug('web', 'handleLoginRequest: Server under maintenance.');
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
return;
}
// If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
var q = require('url').parse(req.url, true);
if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
}
if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
// Login using SSPI
domain.sspi.authenticate(req, res, function (err) {
if ((err != null) || (req.connection.user == null)) {
obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
parent.debug('web', 'handleRootRequest: SSPI auth required.');
try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
} else {
parent.debug('web', 'handleRootRequest: SSPI auth ok.');
handleRootRequestEx(req, res, domain, direct);
}
});
} else if (req.query.user && req.query.pass) {
// User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
// 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
var user = obj.users[userid];
if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
handleRootRequestEx(req, res, domain, direct);
} else if ((userid != null) && (err == null)) {
// Login success
parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
req.session.userid = userid;
delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
handleRootRequestEx(req, res, domain, direct);
} else {
// Login failed
handleRootRequestEx(req, res, domain, direct);
}
});
} else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
// Check if the loginToken is still valid
obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
handleRootRequestEx(req, res, domain, direct); // Login using a different system
});
} else {
// Login using a different system
handleRootRequestEx(req, res, domain, direct);
}
}
function handleRootRequestEx(req, res, domain, direct) {
var nologout = false, user = null;
res.set({ 'Cache-Control': 'no-store' });
// Check if we have an incomplete domain name in the path
if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
res.redirect(domain.url + getQueryPortion(req)); // BAD***
return;
}
if (obj.args.nousers == true) {
// If in single user mode, setup things here.
delete req.session.loginmode;
req.session.userid = 'user/' + domain.id + '/~';
delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
if (obj.users[req.session.userid] == null) {
// Create the dummy user ~ with impossible password
parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
obj.db.SetUser(obj.users[req.session.userid]);
}
} else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
// If a default user is active, setup the session here.
parent.debug('web', 'handleRootRequestEx: auth using default user.');
delete req.session.loginmode;
req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
} else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
//if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
// If a login cookie was provided, setup the session here.
parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
delete req.session.loginmode;
req.session.userid = loginCookie.u;
delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
} else {
parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
}
} else if (domain.sspi != null) {
// SSPI login (Windows only)
//console.log(req.connection.user, req.connection.userSid);
if ((req.connection.user == null) || (req.connection.userSid == null)) {
parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
res.sendStatus(404); return;
} else {
nologout = true;
req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
req.session.usersid = req.connection.userSid;
req.session.usersGroups = req.connection.userGroups;
delete req.session.currentNode;
req.session.ip = req.clientIp; // Bind this session to the IP address of the request
setSessionRandom(req);
obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
// Check if this user exists, create it if not.
user = obj.users[req.session.userid];
if ((user == null) || (user.sid != req.session.usersid)) {
// Create the domain user
var usercount = 0, user2 = { type: 'user', _id: req.session.userid, name: req.connection.user, domain: domain.id, sid: req.session.usersid, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000) };
if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
// Auto-join any user groups
if (typeof domain.newaccountsusergroups == 'object') {
for (var i in domain.newaccountsusergroups) {
var ugrpid = domain.newaccountsusergroups[i];
if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
var ugroup = obj.userGroups[ugrpid];
if (ugroup != null) {
// Add group to the user
if (user2.links == null) { user2.links = {}; }
user2.links[ugroup._id] = { rights: 1 };
// Add user to the group
ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
db.Set(ugroup);
// Notify user group change
var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user2.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
}
}
}
obj.users[req.session.userid] = user2;
obj.db.SetUser(user2);
var event = { etype: 'user', userid: req.session.userid, username: req.connection.user, account: obj.CloneSafeUser(user2), action: 'accountcreate', msg: 'Domain account created, user ' + req.connection.user, domain: domain.id };
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
}
}
}
// Figure out the minimal password requirement
var passRequirements = null;
if (domain.passwordrequirements != null) {
if (domain.passrequirementstr == null) {
var passRequirements = {};
if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
}
passRequirements = domain.passwordrequirementsstr;
}
// If a user exists and is logged in, serve the default app, otherwise server the login app.
if (req.session && req.session.userid && obj.users[req.session.userid]) {
const user = obj.users[req.session.userid];
// Check if we are in maintenance mode
if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
req.session.messageid = 115; // Server under maintenance
req.session.loginmode = 1;
res.redirect(domain.url);
return;
}
// If the request has a "meshmessengerid", redirect to MeshMessenger
// This situation happens when you get a push notification for a chat session, but are not logged in.
if (req.query.meshmessengerid != null) {
res.redirect(domain.url + 'messenger?id=' + req.query.meshmessengerid + ((req.query.key != null) ? ('&key=' + req.query.key) : ''));
return;
}
const xdbGetFunc = function dbGetFunc(err, states) {
if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
parent.debug('web', 'handleRootRequestEx: incorrect domain.');
dbGetFunc.req.session = null;
dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
return;
}
// Check if this is a locked account
if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
// Locked account
parent.debug('web', 'handleRootRequestEx: locked account.');
delete dbGetFunc.req.session.userid;
delete dbGetFunc.req.session.currentNode;
delete dbGetFunc.req.session.passhint;
delete dbGetFunc.req.session.cuserid;
dbGetFunc.req.session.messageid = 110; // Account locked.
dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
return;
}
var viewmode = 1;
if (dbGetFunc.req.session.viewmode) {
viewmode = dbGetFunc.req.session.viewmode;
delete dbGetFunc.req.session.viewmode;
} else if (dbGetFunc.req.query.viewmode) {
viewmode = dbGetFunc.req.query.viewmode;
}
var currentNode = '';
if (dbGetFunc.req.session.currentNode) {
currentNode = dbGetFunc.req.session.currentNode;
delete dbGetFunc.req.session.currentNode;
} else if (dbGetFunc.req.query.node) {
currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
}
var logoutcontrols = {};
if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
// Give the web page a list of supported server features for this domain and user
const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
// Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
// Send the main web application
var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
if ((!obj.args.user) && (obj.args.nousers != true) && (nologout == false)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
// Clean up the U2F challenge if needed
if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
if (dbGetFunc.req.session.e) {
const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
}
// Intel AMT Scanning options
var amtscanoptions = '';
if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
// Fetch the web state
parent.debug('web', 'handleRootRequestEx: success.');
var webstate = '';
if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
if ((webstate == '') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
var webstate2 = {};
try { if (webstate != '') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
webstate = JSON.stringify(webstate2);
}
// Custom user interface
var customui = '';
if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
// Server features
var serverFeatures = 255;
if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
else if (typeof domain.myserver == 'object') {
if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
}
if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
}
// Refresh the session
render(dbGetFunc.req, dbGetFunc.res, getRenderPage('default', dbGetFunc.req, domain), getRenderArgs({
authCookie: authCookie,
authRelayCookie: authRelayCookie,
viewmode: viewmode,
currentNode: currentNode,
logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
domain: domain.id,
debuglevel: parent.debugLevel,
serverDnsName: obj.getWebServerName(domain, req),
serverRedirPort: args.redirport,
serverPublicPort: httpsPort,
serverfeatures: serverFeatures,
features: allFeatures.features,
features2: allFeatures.features2,
sessiontime: (args.sessiontime) ? args.sessiontime : 60,
mpspass: args.mpspass,
passRequirements: passRequirements,
customui: customui,
webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
footer: (domain.footer == null) ? '' : domain.footer,
webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
amtscanoptions: amtscanoptions,
pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
webRelayPort: ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0)),
webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
showNotesPanel: (domain.shownotespanel ? 'true' : 'false')
}, dbGetFunc.req, domain), user);
}
xdbGetFunc.req = req;
xdbGetFunc.res = res;
xdbGetFunc.user = user;
obj.db.Get('ws' + user._id, xdbGetFunc);
} else {
// Send back the login application
// If this is a 2 factor auth request, look for a hardware key challenge.
// Normal login 2 factor request
if (req.session && (req.session.loginmode == 4)) {
const sec = parent.decryptSessionData(req.session.e);
if ((sec != null) && (typeof sec.tuserid == 'string')) {
const user = obj.users[sec.tuserid];
if (user != null) {
parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
return;
}
}
}
// Password recovery 2 factor request
if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
if ((err != null) || (docs.length == 0)) {
parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
req.session = null;
res.redirect(domain.url + getQueryPortion(req)); // BAD***
} else {
var user = obj.users[docs[0]._id];
if (user != null) {
parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
} else {
parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
req.session = null;
res.redirect(domain.url + getQueryPortion(req)); // BAD***
}
}
});
return;
}
handleRootRequestLogin(req, res, domain, '', passRequirements);
}
}
// Return a list of server supported features for a given domain and user
obj.getDomainUserFeatures = function (domain, user, req) {
var features = 0;
var features2 = 0;
if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
// 0x00000100 --> This feature flag is free for future use.
if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
if ((parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (user._id.split('/')[2][0] != '~')) { features += 0x00001000; } // 2FA login supported
if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
// Check if we can skip 2nd factor auth because of the source IP address
var skip2factor = false;
if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
for (var i in domain.passwordrequirements.skip2factor) {
if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
}
}
if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
}
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group.
if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || (domain.passwordrequirements.logintokens.indexOf(user._id) >= 0)))) { features2 += 0x00000080; } // Indicates login tokens are allowed
if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
if ((typeof domain.files == 'object') && (domain.files.sftpconnect === false)) { features2 += 0x00800000; } // Remove the "SFTP Connect" button in the "Files" tab when the device is agent managed
if ((typeof domain.terminal == 'object') && (domain.terminal.sshconnect === false)) { features2 += 0x01000000; } // Remove the "SSH Connect" button in the "Terminal" tab when the device is agent managed
if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
return { features: features, features2: features2 };
}
function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
parent.debug('web', 'handleRootRequestLogin()');
var features = 0;
if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string'))) { features += 32; } // Allow site within iframe
if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
var loginmode = 0;
if (req.session) { loginmode = req.session.loginmode; delete req.session.loginmode; } // Clear this state, if the user hits refresh, we want to go back to the login page.
// Format an error message if needed
var passhint = null, msgid = 0;
if (req.session != null) {
msgid = req.session.messageid;
if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
delete req.session.messageid;
delete req.session.passhint;
}
const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
const emailcheck = (allowAccountReset && (domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
// Check if we are allowed to create new users using the login screen
var newAccountsAllowed = true;
if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
// Encrypt the hardware key challenge state if needed
var hwstate = null;
if (hardwareKeyChallenge && req.session) {
const sec = parent.decryptSessionData(req.session.e);
hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
}
// Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
// See if we support two-factor trusted cookies
var twoFactorCookieDays = 30;
if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
// See what authentication strategies we have
var authStrategies = [];
if (typeof domain.authstrategies == 'object') {
if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
if (typeof domain.authstrategies.oidc == 'object') {
if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
} else {
authStrategies.push('oidc');
}
}
if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
}
// Custom user interface
var customui = '';
if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
// Get two-factor screen timeout
var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
}
// Setup CAPTCHA if needed
var newAccountCaptcha = '', newAccountCaptchaImage = '';
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
}
// Render the login page
render(req, res,
getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
getRenderArgs({
loginmode: loginmode,
rootCertLink: getRootCertLink(domain),
newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
serverDnsName: obj.getWebServerName(domain, req),
serverPublicPort: httpsPort,
passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
emailcheck: emailcheck,
features: features,
sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
passRequirements: passRequirements,
customui: customui,
footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
messageid: msgid,
passhint: passhint,
welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null,
welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
hwstate: hwstate,
otpemail: otpemail,
otpsms: otpsms,
otpmsg: otpmsg,
otppush: otppush,
autofido: autofido,
twoFactorCookieDays: twoFactorCookieDays,
authStrategies: authStrategies.join(','),
loginpicture: (typeof domain.loginpicture == 'string'),
tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
renderLanguages: obj.renderLanguages,
showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
}, req, domain, (domain.sitestyle == 2) ? 'login2' : 'login'));
}
// Handle a post request on the root
function handleRootPostRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
if (req.body == null) { req.body = {}; }
parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
// If a HTTP header is required, check new UserRequiredHttpHeader
if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
switch (req.body.action) {
case 'login': { handleLoginRequest(req, res, true); break; }
case 'tokenlogin': {
if (req.body.hwstate) {
var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
}
handleLoginRequest(req, res, true); break;
}
case 'pushlogin': {
if (req.body.hwstate) {
var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
// Push authentication is a success, login the user
req.session = { userid: cookie.u };
// Check if we need to remember this device
if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
var maxCookieAge = domain.twofactorcookiedurationdays;
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
}
handleRootRequestEx(req, res, domain);
return;
}
}
handleLoginRequest(req, res, true); break;
}
case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
default: { handleLoginRequest(req, res, true); break; }
}
}
// Return true if it looks like we are using a real TLS certificate.
obj.isTrustedCert = function (domain) {
if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
if (obj.parent.config.letsencrypt != null) return (obj.parent.config.letsencrypt.production === true); // We are using Let's Encrypt, real cert in use if production is set to true.
if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
return true; // This is a guess
}
// Get the link to the root certificate if needed
function getRootCertLink(domain) {
// Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
if (obj.isTrustedCert(domain) == false) {
// Get the domain suffix
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
}
return '';
}
// Serve the xterm page
function handleXTermRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
parent.debug('web', 'handleXTermRequest: sending xterm');
res.set({ 'Cache-Control': 'no-store' });
if (req.session && req.session.userid) {
if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
var user = obj.users[req.session.userid];
if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
// Check permissions
obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
var logoutcontrols = { name: user.name };
var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
// Create a authentication cookie
const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
render(req, res, getRenderPage('xterm', req, domain), getRenderArgs({ serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, authCookie: authCookie, authRelayCookie: authRelayCookie, logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'), name: EscapeHtml(node.name) }, req, domain));
});
} else {
res.redirect(domain.url + getQueryPortion(req));
return;
}
}
// Handle new account Captcha GET
function handleNewAccountCaptchaRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
res.type('svg');
res.status(200).end(require('svg-captcha')(c.captcha, {}));
}
// Handle Captcha GET
function handleCaptchaGetRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
}
// Handle Captcha POST
function handleCaptchaPostRequest(req, res) {
if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
}
// Render the terms of service.
function handleTermsRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
// See if term.txt was loaded from the database
if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
// Send the terms from the database
res.set({ 'Cache-Control': 'no-store' });
if (req.session && req.session.userid) {
if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
var user = obj.users[req.session.userid];
var logoutcontrols = { name: user.name };
var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
} else {
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
}
} else {
// See if there is a terms.txt file in meshcentral-data
var p = obj.path.join(obj.parent.datapath, 'terms.txt');
if (obj.fs.existsSync(p)) {
obj.fs.readFile(p, 'utf8', function (err, data) {
if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
// Send the terms from terms.txt
res.set({ 'Cache-Control': 'no-store' });
if (req.session && req.session.userid) {
if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
var user = obj.users[req.session.userid];
var logoutcontrols = { name: user.name };
var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
} else {
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
}
});
} else {
// Send the default terms
parent.debug('web', 'handleTermsRequest: sending default terms');
res.set({ 'Cache-Control': 'no-store' });
if (req.session && req.session.userid) {
if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
var user = obj.users[req.session.userid];
var logoutcontrols = { name: user.name };
var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
} else {
render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
}
}
}
}
// Render the messenger application.
function handleMessengerRequest(req, res) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
parent.debug('web', 'handleMessengerRequest()');
// Check if we are in maintenance mode
if (parent.config.settings.maintenancemode != null) {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
return;
}
// Check if this session is for a user
if (req.query.id == null) { res.sendStatus(404); return; }
var idSplit = decodeURIComponent(req.query.id).split('/');
if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
// This is a user to user conversation, both users must be logged in.
var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
if (!req.session || !req.session.userid) {
// Redirect to login page
if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key + '&meshmessengerid=' + req.query.id); } else { res.redirect(domain.url + '?meshmessengerid=' + req.query.id); }
return;
}
if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
}
// Get WebRTC configuration
var webRtcConfig = null;
if (obj.parent.config.settings && obj.parent.config.settings.webrtconfig && (typeof obj.parent.config.settings.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtconfig)).replace(/'/g, '%27'); }
else if (args.webrtconfig && (typeof args.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtconfig)).replace(/'/g, '%27'); }
// Setup other options
var options = { webrtconfig: webRtcConfig };
if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
// Get the userid and name
if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
if (idSplit.length == 7) {
const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
if (user != null) {
if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
}
}
}
// Render the page
res.set({ 'Cache-Control': 'no-store' });
render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
}
// Handle messenger image request
function handleMessengerImageRequest(req, res) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
parent.debug('web', 'handleMessengerImageRequest()');
// Check if we are in maintenance mode
if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
//res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
if (domain.meshmessengerpicture) {
// Use the configured messenger logo picture
try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
}
var imagefile = 'images/messenger.png';
if (domain.webpublicpath != null) {
obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
if (exists) {
// Use the domain logo picture
try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
});
} else if (parent.webPublicOverridePath) {
obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
if (exists) {
// Use the override logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
});
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
}
// Returns the server root certificate encoded in base64
function getRootCertBase64() {
var rootcert = obj.certificates.root.cert;
var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
if (i >= 0) { rootcert = rootcert.substring(i + 29); }
i = rootcert.indexOf('-----END CERTIFICATE-----');
if (i >= 0) { rootcert = rootcert.substring(i, 0); }
return Buffer.from(rootcert, 'base64').toString('base64');
}
// Returns the mesh server root certificate
function handleRootCertRequest(req, res) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
parent.debug('web', 'handleRootCertRequest()');
setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
res.send(Buffer.from(getRootCertBase64(), 'base64'));
}
// Handle user public file downloads
function handleDownloadUserFiles(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
if (domain.id != '') { domainname = 'domain-' + domain.id; }
var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
for (var i = 3; i < spliturl.length; i++) { if (obj.common.IsFilenameValid(spliturl[i]) == true) { path += '/' + spliturl[i]; filename = spliturl[i]; } else { res.sendStatus(404); return; } }
var stat = null;
try { stat = obj.fs.statSync(path); } catch (e) { }
if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
if (req.query.download == 1) {
setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
} else {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
}
} else {
render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
}
}
// Handle device file request
function handleDeviceFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
// Check the inbound desktop sharing cookie
var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
// Check userid
const user = obj.users[c.userid];
if ((c == user)) { res.sendStatus(404); return; }
// If this cookie has restricted usages, check that it's allowed to perform downloads
if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
if (req.query.n == null) { res.sendStatus(404); return; }
// Check if this user has permission to manage this computer
obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
// All good, start the file transfer
req.query.id = getRandomLowerCase(12);
obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
});
}
// Handle download of a server file by an agent
function handleAgentDownloadFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (req.query.c == null) { res.sendStatus(404); return; }
// Check the inbound desktop sharing cookie
var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
if ((c == null) || (c.a != 'tmpdl') || (c.d != domain.id) || (c.nid == null) || (c.f == null) || (obj.common.IsFilenameValid(c.f) == false)) { res.sendStatus(404); return; }
// Send the file back
try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
}
// Handle logo request
function handleLogoRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
//res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
if (domain.titlepicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
// Use the logo in the database
res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
res.send(parent.configurationFiles[domain.titlepicture]);
return;
} else {
// Use the logo on file
try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
}
}
if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
// Use the domain logo picture
try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
} else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
// Use the override logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
}
}
// Handle login logo request
function handleLoginLogoRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
//res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
if (domain.loginpicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
// Use the logo in the database
res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
res.send(parent.configurationFiles[domain.loginpicture]);
return;
} else {
// Use the logo on file
try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
}
} else {
res.sendStatus(404);
}
}
// Handle translation request
function handleTranslationsRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
//if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
var user = null;
if (obj.args.user != null) {
// A default user is active
user = obj.users['user/' + domain.id + '/' + obj.args.user];
if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
} else {
// Check if the user is logged and we have all required parameters
if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
// Get the current user
user = obj.users[req.session.userid];
if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
}
var data = '';
req.setEncoding('utf8');
req.on('data', function (chunk) { data += chunk; });
req.on('end', function () {
try { data = JSON.parse(data); } catch (ex) { data = null; }
if (data == null) { res.sendStatus(404); return; }
if (data.action == 'getTranslations') {
if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
// Return the translation file (JSON)
try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
} else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
// Return the default translation file (JSON)
try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
} else { res.sendStatus(404); }
} else if (data.action == 'setTranslations') {
obj.fs.writeFile(obj.path.join(obj.parent.datapath, 'translate.json'), obj.common.translationsToJson({ strings: data.strings }), function (err) { if (err == null) { res.send(JSON.stringify({ response: 'ok' })); } else { res.send(JSON.stringify({ response: err })); } });
} else if (data.action == 'translateServer') {
if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
res.send(JSON.stringify({ response: 'ok' }));
console.log('Started server translation...');
obj.pendingTranslation = true;
require('child_process').exec('node translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
delete obj.pendingTranslation;
//console.log('error', error);
//console.log('stdout', stdout);
//console.log('stderr', stderr);
//console.log('Server restart...'); // Perform a server restart
//process.exit(0);
console.log('Server translation completed.');
});
} else {
// Unknown request
res.sendStatus(404);
}
});
}
// Handle welcome image request
function handleWelcomeImageRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
//res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
if (domain.welcomepicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
// Use the welcome image in the database
res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
res.send(parent.configurationFiles[domain.welcomepicture]);
return;
}
// Use the configured logo picture
try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
}
var imagefile = 'images/mainwelcome.jpg';
if (domain.sitestyle == 2) { imagefile = 'images/login/back.png'; }
if (domain.webpublicpath != null) {
obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
if (exists) {
// Use the domain logo picture
try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
});
} else if (parent.webPublicOverridePath) {
obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
if (exists) {
// Use the override logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
});
} else {
// Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
}
}
// Download a session recording
function handleGetRecordings(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) return;
// Check the query
if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; }
// Get the recording path
var recordingsPath = null;
if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
if (recordingsPath == null) { res.sendStatus(401); return; }
// Get the user and check user rights
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
if (authUserid == null) { res.sendStatus(401); return; }
const user = obj.users[authUserid];
if (user == null) { res.sendStatus(401); return; }
if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
// Send the recorded file
setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
}
// Stream a session recording
function handleGetRecordingsWebSocket(ws, req) {
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; }
// Check the query
if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; }
// Get the recording path
var recordingsPath = null;
if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
// Get the user and check user rights
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
const user = obj.users[authUserid];
if (user == null) { try { ws.close(); } catch (ex) { } return; }
if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
const filefullpath = obj.path.join(recordingsPath, req.query.file);
obj.fs.stat(filefullpath, function (err, stats) {
if (err) {
try { ws.close(); } catch (ex) { } // File does not exist
} else {
obj.fs.open(filefullpath, 'r', function (err, fd) {
if (err == null) {
// When data is received from the web socket
ws.on('message', function (msg) {
if (typeof msg != 'string') return;
var command;
try { command = JSON.parse(msg); } catch (e) { return; }
if ((command == null) || (typeof command.action != 'string')) return;
switch (command.action) {
case 'get': {
const buffer = Buffer.alloc(8 + command.size);
//buffer.writeUInt32BE((command.ptr >> 32), 0);
buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); });
break;
}
}
});
// If error, do nothing
ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
// If the web socket is closed
ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
} else {
try { ws.close(); } catch (ex) { }
}
});
}
});
}
// Serve the player page
function handlePlayerRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
parent.debug('web', 'handlePlayerRequest: sending player');
res.set({ 'Cache-Control': 'no-store' });
render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
}
// Serve the guest sharing page
function handleSharingRequest(req, res) {
const domain = getDomain(req, res);
if (domain == null) { return; }
if (req.query.c == null) { res.sendStatus(404); return; }
if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
// Check the inbound guest sharing cookie
var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
if (c == null) { res.sendStatus(404); return; }
if (c.a === 5) {
// This is the older style sharing cookie with everything encoded within it.
// This cookie style gives a very large URL, so it's not used anymore.
if ((typeof c.p !== 'number') || (c.p < 1) || (c.p > 7) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
handleSharingRequestEx(req, res, domain, c);
return;
}
if (c.a === 6) {
// This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
// Gives a much more compact URL.
if (typeof c.pid != 'string') { res.sendStatus(404); return; }
// Check the expired time, expire message.
if ((c.e != null) && (c.e <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
const doc = docs[0];
// If this is a recurrent share, check if we are at the correct time to make use of it
if (typeof doc.recurring == 'number') {
const now = Date.now();
if (now >= doc.startTime) { // We don't want to move the validity window before the start time
const deltaTime = (now - doc.startTime);
if (doc.recurring === 1) {
// This moves the start time to the next valid daily window
const oneDay = (24 * 60 * 60 * 1000);
var addition = Math.floor(deltaTime / oneDay);
if ((deltaTime - (addition * oneDay)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
doc.startTime += (addition * oneDay);
} else if (doc.recurring === 2) {
// This moves the start time to the next valid weekly window
const oneWeek = (7 * 24 * 60 * 60 * 1000);
var addition = Math.floor(deltaTime / oneWeek);
if ((deltaTime - (addition * oneWeek)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
doc.startTime += (addition * oneWeek);
}
}
}
// Generate an old style cookie from the information in the database
var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey ? doc.extrakey : null, port: doc.port };
if (doc.userid) { cookie.uid = doc.userid; }
if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
if (doc.startTime != null) {
if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
}
if (doc.viewOnly === true) { cookie.vo = 1; }
handleSharingRequestEx(req, res, domain, cookie);
});
return;
}
res.sendStatus(404); return;
}
// Serve the guest sharing page
function handleSharingRequestEx(req, res, domain, c) {
// Check the expired time, expire message.
if ((c.expire != null) && (c.expire <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
// Check the public id
obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
// Check if any sharing links are present, expire message.
if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
// Search for the device share public identifier, expire message.
var found = false;
for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == c.pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === c.k))) { found = true; } }
if (found == false) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
// Get information about this node
obj.db.Get(c.nid, function (err, nodes) {
if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
var node = nodes[0];
// Check the start time, not yet valid message.
if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
// If this is a web relay share, check if this feature is active
if ((c.p == 8) || (c.p == 16)) {
// This is a HTTP or HTTPS share
var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0));
if (webRelayPort == 0) { res.sendStatus(404); return; }
// Create the authentication cookie
const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, r: 8, expire: c.expire, pid: c.pid, port: c.port };
if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
// Redirect to a URL
var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
if (c.addr != null) { url += '&addr=' + c.addr; }
if (c.pid != null) { url += '&relayid=' + c.pid; }
parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
res.redirect(url);
} else {
// Looks good, let's create the outbound session cookies.
// This is a desktop, terminal or files share. We need to display the sharing page.
// Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo };
if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
if (c.k != null) { authCookieData.k = c.k; }
const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
// Server features
var features2 = 0;
if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
// Lets respond by sending out the desktop viewer.
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
res.set({ 'Cache-Control': 'no-store' });
render(req, res, getRenderPage('sharing', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire, viewOnly: (c.vo == 1) ? 1 : 0, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain));
}
});
});
}
// Handle domain redirection
obj.handleDomainRedirect = function (req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.redirects == null) { res.sendStatus(404); return; }
var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
if (domain.redirects[urlName] == '~showversion') {
// Show the current version
res.end('MeshCentral v' + obj.parent.currentVer);
} else {
// Perform redirection
res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
}
}
// Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
obj.getServerFilePath = function (user, domain, path) {
var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
for (var i = 3; i < splitpath.length; i++) { if (obj.common.IsFilenameValid(splitpath[i]) == true) { serverpath += '/' + splitpath[i]; filename = splitpath[i]; } else { return null; } } // Check that each folder is correct
return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
};
// Return the maximum number of bytes allowed in the user account "My Files".
obj.getQuota = function (objid, domain) {
if (objid == null) return 0;
if (objid.startsWith('user/')) {
var user = obj.users[objid];
if (user == null) return 0;
if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
return null; // By default, the user will have no limit
} else if (objid.startsWith('mesh/')) {
var mesh = obj.meshes[objid];
if (mesh == null) return 0;
if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
return null; // By default, the mesh will have no limit
}
return 0;
};
// Download a file from the server
function handleDownloadFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
const user = obj.users[req.session.userid];
if (user == null) { res.sendStatus(404); return; }
const file = obj.getServerFilePath(user, domain, req.query.link);
if (file == null) { res.sendStatus(404); return; }
setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
}
// Download the MeshCommander web page
function handleMeshCommander(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
// Find the correct MeshCommander language to send
const acceptableLanguages = obj.getLanguageCodes(req);
const commandLanguageTranslations = { 'en': '', 'de': '-de', 'es': '-es', 'fr': '-fr', 'it': '-it', 'ja': '-ja', 'ko': '-ko', 'nl': '-nl', 'pt': '-pt', 'ru': '-ru', 'zh-chs': '-zh-chs', 'zh-cht': '-zh-chs' };
for (var i in acceptableLanguages) {
const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]];
if (meshCommanderLanguage != null) {
try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { }
return;
}
}
// Send out the default english MeshCommander
try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
}
// Upload a MeshCore.js file to the server
function handleUploadMeshCoreFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.id !== '') { res.sendStatus(401); return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
// Get the user
const user = obj.users[authUserid];
if (user == null) { res.sendStatus(401); return; } // Check this user exists
// Get the node and check node rights
const nodeid = fields.attrib[0];
obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
for (var i in files.files) {
var file = files.files[i];
obj.fs.readFile(file.path, 'utf8', function (err, data) {
if (err != null) return;
data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
try { obj.fs.unlinkSync(file.path); } catch (e) { }
});
}
res.send('');
});
});
}
// Upload a MeshCore.js file to the server
function handleOneClickRecoveryFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.id !== '') { res.sendStatus(401); return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
// Get the user
const user = obj.users[authUserid];
if (user == null) { res.sendStatus(401); return; } // Check this user exists
// Get the node and check node rights
const nodeid = fields.attrib[0];
obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
for (var i in files.files) {
var file = files.files[i];
// Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
//try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
}
res.send('');
});
});
}
// Upload a file to the server
function handleUploadFile(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if (domain.userQuota == -1) { res.sendStatus(401); return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
// Get the user
const user = obj.users[authUserid];
if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
var xfile = null;
try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
if (xfile == null) { res.sendStatus(404); return; }
// Get total bytes in the path
var totalsize = readTotalFileSize(xfile.fullpath);
if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
if (fields.name != null) {
// See if we need to create the folder
var domainx = 'domain';
if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
// Upload method where all the file data is within the fields.
var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
for (var i = 0; i < names.length; i++) {
if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
// Create the user folder if needed
(function (fullpath, filename, filedata) {
obj.fs.mkdir(xfile.fullpath, function () {
// Write the file
obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
});
});
})(xfile.fullpath, names[i], filedata);
} else {
// Send a notification
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
}
}
}
} else {
// More typical upload method, the file data is in a multipart mime post.
for (var i in files.files) {
var file = files.files[i], fpath = obj.path.join(xfile.fullpath, file.originalFilename);
if (obj.common.IsFilenameValid(file.originalFilename) && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
// See if we need to create the folder
var domainx = 'domain';
if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
// Rename the file
obj.fs.rename(file.path, fpath, function (err) {
if (err && (err.code === 'EXDEV')) {
// On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
obj.common.copyFile(file.path, fpath, function (err) {
obj.fs.unlink(file.path, function (err) {
obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
});
});
} else {
obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
}
});
} else {
// Send a notification
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
}
}
}
} else {
// Send a notification
obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
}
res.send('');
});
}
// Upload a file to the server and then batch upload to many agents
function handleUploadFileBatch(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
// Get the user
const user = obj.users[authUserid];
if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
// Get fields
if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
// Check if we have at least one target path
if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
res.send('');
return;
}
// Get server temporary path
var serverpath = obj.path.join(obj.filespath, 'tmp')
try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
// More typical upload method, the file data is in a multipart mime post.
for (var i in files.files) {
var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
cmd.files.push({ name: file.originalFilename, target: ftarget });
// Rename the file
obj.fs.rename(file.path, fpath, function (err) {
if (err && (err.code === 'EXDEV')) {
// On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
}
});
}
// Instruct one of more agents to download a URL to a given local drive location.
var tlsCertHash = null;
if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
}
for (var i in cmd.nodeids) {
obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
if (agentPath == null) return;
// Compute user consent
var consent = 0;
var mesh = obj.meshes[node.meshid];
if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
// Check if we need to add consent flags because of a user group link
if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
// This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
for (var i in user.links) {
var ugrp = obj.userGroups[i];
if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
consent |= ugrp.consent; // Add user group consent flags
}
}
}
// Event that this operation is being performed.
var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
var event = { etype: 'node', userid: cmd.user._id, username: cmd.user.name, nodeid: node._id, action: 'batchupload', msg: 'Performing batch upload of ' + cmd.files.length + ' file(s) to ' + agentPath, msgid: msgid, msgArgs: [cmd.files.length, agentPath], domain: cmd.domain.id };
parent.DispatchEvent(targets, obj, event);
// Send the agent commands to perform the batch upload operation
for (var f in cmd.files) {
if (cmd.files[f].name != null) {
const acmd = { action: 'wget', userid: user._id, username: user.name, realname: user.realname, remoteaddr: req.clientIp, consent: consent, rights: rights, overwrite: cmd.overwrite, createFolder: cmd.createFolder, urlpath: '/agentdownload.ashx?c=' + obj.parent.encodeCookie({ a: 'tmpdl', d: cmd.domain.id, nid: node._id, f: cmd.files[f].target }, obj.parent.loginCookieEncryptionKey), path: obj.path.join(agentPath, cmd.files[f].name), folder: agentPath, servertlshash: tlsCertHash };
var agent = obj.wsagents[node._id];
if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
// TODO: Add support for peer servers.
}
}
});
}
res.send('');
});
}
// Subscribe to all events we are allowed to receive
obj.subscribe = function (userid, target) {
const user = obj.users[userid];
if (user == null) return;
const subscriptions = [userid, 'server-allusers'];
if (user.siteadmin != null) {
// Allow full site administrators of users with all events rights to see all events.
if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
else if ((user.siteadmin & 2) != 0) {
if ((user.groups == null) || (user.groups.length == 0)) {
// Subscribe to all user changes
subscriptions.push('server-users');
} else {
// Subscribe to user changes for some groups
for (var i in user.groups) { subscriptions.push('server-users:' + i); }
}
}
}
if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
obj.parent.RemoveAllEventDispatch(target);
obj.parent.AddEventDispatch(subscriptions, target);
return subscriptions;
};
// Handle a web socket relay request
function handleRelayWebSocket(ws, req, domain, user, cookie) {
if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
// Fetch information about the target
obj.db.Get(req.query.host, function (err, docs) {
if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
var node = docs[0];
if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
// Check if this user has permission to manage this computer
if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
// Check what connectivity is available for this node
var state = parent.GetConnectivityState(req.query.host);
var conn = 0;
if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
// Check what server needs to handle this connection
if ((obj.parent.multiServer != null) && ((cookie == null) || (cookie.ps != 1))) { // If a cookie is provided and is from a peer server, don't allow the connection to jump again to a different server
var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
if (server != null) {
if (server.serverid != obj.parent.serverId) {
// Do local Intel CIRA routing using a different server
parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
return;
}
} else {
server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
if ((server != null) && (server.serverid != obj.parent.serverId)) {
// Do local Intel AMT routing using a different server
parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
return;
}
}
}
// Setup session recording if needed
if (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf((req.query.p == 2) ? 101 : 100) >= 0)))) { // TODO 100
// Check again if we need to do recording
var record = true;
// Check user or device group recording
if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
record = false;
// Check device group recording
if (domain.sessionrecording.onlyselecteddevicegroups === true) {
var mesh = obj.meshes[node.meshid];
if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
}
// Check user recording
if (domain.sessionrecording.onlyselectedusers === true) {
if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
}
}
if (record == true) {
var now = new Date(Date.now());
var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth() + 1, 2) + '-' + obj.common.zeroPad(now.getUTCDate(), 2) + '-' + obj.common.zeroPad(now.getUTCHours(), 2) + '-' + obj.common.zeroPad(now.getUTCMinutes(), 2) + '-' + obj.common.zeroPad(now.getUTCSeconds(), 2) + '-' + getRandomPassword() + '.mcrec'
var recFullFilename = null;
if (domain.sessionrecording.filepath) {
try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
} else {
try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
recFullFilename = obj.path.join(parent.recordpath, recFilename);
}
var fd = obj.fs.openSync(recFullFilename, 'w');
if (fd != null) {
// Write the recording file header
var firstBlock = JSON.stringify({ magic: 'MeshCentralRelaySession', ver: 1, userid: user._id, username: user.name, ipaddr: req.clientIp, nodeid: node._id, intelamt: true, protocol: (req.query.p == 2) ? 101 : 100, time: new Date().toLocaleString() })
recordingEntry(fd, 1, 0, firstBlock, function () { });
ws.logfile = { fd: fd, lock: false };
if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
}
}
}
// If Intel AMT CIRA connection is available, use it
var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
if (ciraconn != null) {
parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
// TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
// Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
var port = 16993;
//if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
if (req.query.p == 2) port += 2;
// Setup a new CIRA channel
if ((port == 16993) || (port == 16995)) {
// Perform TLS
var ser = new SerialTunnel();
var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
// Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
// Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
// When APF tunnel return something, update SerialTunnel buffer
chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
// Handle CIRA tunnel state change
chnl.onStateChange = function (ciraconn, state) {
parent.debug('webrelay', 'Relay TLS CIRA state change', state);
if (state == 0) { try { ws.close(); } catch (e) { } }
if (state == 2) {
// TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
const tlsoptions = { socket: ser, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false };
if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
tlsock.chnl = chnl;
tlsock.setEncoding('binary');
tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
// Decrypted tunnel from TLS communication to be forwarded to websocket
tlsock.on('data', function (data) {
// AMT/TLS ---> WS
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
try { ws.send(data); } catch (ex) { }
});
// If TLS is on, forward it through TLSSocket
ws.forwardclient = tlsock;
ws.forwardclient.xtls = 1;
ws.forwardclient.onStateChange = function (ciraconn, state) {
parent.debug('webrelay', 'Relay CIRA state change', state);
if (state == 0) { try { ws.close(); } catch (e) { } }
};
ws.forwardclient.onData = function (ciraconn, data) {
// Run data thru interceptor
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
if (data.length > 0) {
if (ws.logfile == null) {
try { ws.send(data); } catch (e) { }
} else {
// Log to recording file
recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
}
}
};
// TODO: Flow control? (Dont' really need it with AMT, but would be nice)
ws.forwardclient.onSendOk = function (ciraconn) { };
}
};
} else {
// Without TLS
ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
ws.forwardclient.xtls = 0;
ws._socket.resume();
ws.forwardclient.onStateChange = function (ciraconn, state) {
parent.debug('webrelay', 'Relay CIRA state change', state);
if (state == 0) { try { ws.close(); } catch (e) { } }
};
ws.forwardclient.onData = function (ciraconn, data) {
//parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
// Run data thru interceptor
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
//console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
if (data.length > 0) {
if (ws.logfile == null) {
try { ws.send(data); } catch (e) { }
} else {
// Log to recording file
recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
}
}
};
// TODO: Flow control? (Dont' really need it with AMT, but would be nice)
ws.forwardclient.onSendOk = function (ciraconn) { };
}
// When data is received from the web socket, forward the data into the associated CIRA channel.
// If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
ws.on('message', function (data) {
//parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
// WS ---> AMT/TLS
if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
// Log to recording file
if (ws.logfile == null) {
// Forward data to the associated TCP connection.
try { ws.forwardclient.write(data); } catch (ex) { }
} else {
// Log to recording file
recordingEntry(ws.logfile.fd, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
}
});
// If error, close the associated TCP connection.
ws.on('error', function (err) {
console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
parent.debug('webrelay', 'Websocket relay closed on error.');
// Websocket closed, close the CIRA channel and TLS session.
if (ws.forwardclient) {
if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
delete ws.forwardclient;
}
// Close the recording file
if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
});
// If the web socket is closed, close the associated TCP connection.
ws.on('close', function (req) {
parent.debug('webrelay', 'Websocket relay closed.');
// Websocket closed, close the CIRA channel and TLS session.
if (ws.forwardclient) {
if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
delete ws.forwardclient;
}
// Close the recording file
if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
});
// Note that here, req.query.p: 1 = WSMAN with server auth, 2 = REDIR with server auth, 3 = WSMAN without server auth, 4 = REDIR with server auth
// Fetch Intel AMT credentials & Setup interceptor
if (req.query.p == 1) {
parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
ws.interceptor.blockAmtStorage = true;
} else if (req.query.p == 2) {
parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
ws.interceptor.blockAmtStorage = true;
}
return;
}
// If Intel AMT direct connection is possible, option a direct socket
if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
// When data is received from the web socket, forward the data into the associated TCP connection.
ws.on('message', function (msg) {
//parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
// Log to recording file
if (ws.logfile == null) {
// Forward data to the associated TCP connection.
try { ws.forwardclient.write(msg); } catch (ex) { }
} else {
// Log to recording file
recordingEntry(ws.logfile.fd, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
}
});
// If error, close the associated TCP connection.
ws.on('error', function (err) {
console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
// Close the recording file
if (ws.logfile != null) {
recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
obj.fs.close(fd);
ws.logfile = null;
});
}
});
// If the web socket is closed, close the associated TCP connection.
ws.on('close', function () {
parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
// Close the recording file
if (ws.logfile != null) {
recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
obj.fs.close(fd);
ws.logfile = null;
});
}
});
// Compute target port
var port = 16992;
if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
if (node.intelamt.tls == 0) {
// If this is TCP (without TLS) set a normal TCP socket
ws.forwardclient = new obj.net.Socket();
ws.forwardclient.setEncoding('binary');
ws.forwardclient.xstate = 0;
ws.forwardclient.forwardwsocket = ws;
ws._socket.resume();
} else {
// If TLS is going to be used, setup a TLS socket
var tlsoptions = { ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, rejectUnauthorized: false };
if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
// The TLS connection method is the same as TCP, but located a bit differently.
parent.debug('webrelay', 'TLS connected to ' + node.host + ':' + port + '.');
ws.forwardclient.xstate = 1;
ws._socket.resume();
});
ws.forwardclient.setEncoding('binary');
ws.forwardclient.xstate = 0;
ws.forwardclient.forwardwsocket = ws;
}
// When we receive data on the TCP connection, forward it back into the web socket connection.
ws.forwardclient.on('data', function (data) {
if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
if (obj.parent.debugLevel >= 1) { // DEBUG
parent.debug('webrelaydata', 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
//if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
}
if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
if (ws.logfile == null) {
// No logging
try { ws.send(data); } catch (e) { }
} else {
// Log to recording file
recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
}
});
// If the TCP connection closes, disconnect the associated web socket.
ws.forwardclient.on('close', function () {
parent.debug('webrelay', 'TCP relay disconnected from ' + node.host + ':' + port + '.');
try { ws.close(); } catch (e) { }
});
// If the TCP connection causes an error, disconnect the associated web socket.
ws.forwardclient.on('error', function (err) {
parent.debug('webrelay', 'TCP relay error from ' + node.host + ':' + port + ': ' + err);
try { ws.close(); } catch (e) { }
});
// Fetch Intel AMT credentials & Setup interceptor
if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
if (node.intelamt.tls == 0) {
// A TCP connection to Intel AMT just connected, start forwarding.
ws.forwardclient.connect(port, node.host, function () {
parent.debug('webrelay', 'TCP relay connected to ' + node.host + ':' + port + '.');
ws.forwardclient.xstate = 1;
ws._socket.resume();
});
}
return;
}
});
}
// Setup agent to/from server file transfer handler
function handleAgentFileTransfer(ws, req) {
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
if (ws.xcmd == 'coredump') {
// Check the agent core dump folder if not already present.
var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
ws.xid = 'coredump';
ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
}
// When data is received from the web socket, echo it back
ws.on('message', function (data) {
if (typeof data == 'string') {
// Control message
var cmd = null;
try { cmd = JSON.parse(data); } catch (ex) { }
if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
switch (cmd.sub) {
case 'start': {
// Perform an async file open
var callback = function onFileOpen(err, fd) {
onFileOpen.xws.xfile = fd;
try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
};
callback.xws = this;
obj.fs.open(this.xfilepath + '.part', 'w', callback);
break;
}
}
} else {
// Binary message
if (data.length < 4) return;
var flags = data.readInt32BE(0);
if ((data.length > 4)) {
// Write the file
this.xfilelen += (data.length - 4);
try {
var callback = function onFileDataWritten(err, bytesWritten, buffer) {
if (onFileDataWritten.xflags & 1) {
// End of file
parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
if (onFileDataWritten.xws.xfile) {
obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
onFileDataWritten.xws.xfile = null;
}
try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
try { onFileDataWritten.xws.close(); } catch (ex) { }
} else {
// Send ack
try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
}
};
callback.xws = this;
callback.xflags = flags;
obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
} catch (ex) { }
} else {
if (flags & 1) {
// End of file
parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
if (this.xfile) {
obj.fs.close(this.xfile, function (err) { });
obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
this.xfile = null;
}
this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
try { this.close(); } catch (ex) { }
} else {
// Send ack
this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
}
}
}
});
// If error, do nothing.
ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
// If closed, do nothing
ws.on('close', function (req) {
if (this.xfile) {
obj.fs.close(this.xfile, function (err) { });
obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
}
});
}
// Handle the web socket echo request, just echo back the data sent
function handleEchoWebSocket(ws, req) {
const domain = checkUserIpAddress(ws, req);
if (domain == null) { return; }
ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
// When data is received from the web socket, echo it back
ws.on('message', function (data) {
if (data.toString('utf8') == 'close') {
try { ws.close(); } catch (e) { console.log(e); }
} else {
try { ws.send(data); } catch (e) { console.log(e); }
}
});
// If error, do nothing.
ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
// If closed, do nothing
ws.on('close', function (req) { });
}
// Handle the 2FA hold web socket
// Accept an hold a web socket connection until the 2FA response is received.
function handle2faHoldWebSocket(ws, req) {
const domain = checkUserIpAddress(ws, req);
if (domain == null) { return; }
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
if (typeof req.query.c !== 'string') { ws.close(); return; }
const cookie = parent.decodeCookie(req.query.c, null, 1);
if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
var user = obj.users[cookie.u];
if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
// 2FA event subscription
obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
ws.cookie = cookie;
ws.HandleEvent = function (source, event, ids, id) {
obj.parent.RemoveAllEventDispatch(this);
if ((event.approved === true) && (event.userid == this.cookie.u)) {
// Create a login cookie
const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
} else {
// Reject the login
try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
}
}
// We do not accept any data on this connection.
ws.on('message', function (data) { this.close(); });
// If error, do nothing.
ws.on('error', function (err) { });
// If closed, unsubscribe
ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
// Perform push notification to device
try {
const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
var code = Buffer.from(cookie.c, 'base64').toString();
var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) {
try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
}
});
} catch (ex) { console.log(ex); }
}
// Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
function readTotalFileSize(path) {
var r = 0, dir;
try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
for (var i in dir) {
var stat = obj.fs.statSync(path + '/' + dir[i]);
if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
}
return r;
}
// Delete a folder and all sub items. (TODO: try to make all async version)
function deleteFolderRec(path) {
if (obj.fs.existsSync(path) == false) return;
try {
obj.fs.readdirSync(path).forEach(function (file, index) {
var pathx = path + '/' + file;
if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
});
obj.fs.rmdirSync(path);
} catch (ex) { }
}
// Handle Intel AMT events
// To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
obj.handleAmtEventRequest = function (req, res) {
const domain = getDomain(req);
try {
if (req.headers.authorization) {
var authstr = req.headers.authorization;
if (authstr.substring(0, 7) == 'Digest ') {
var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
// Read the data, we need to get the arg field
var eventData = '';
req.on('data', function (chunk) { eventData += chunk; });
req.on('end', function () {
// Completed event read, let get the argument that must contain the nodeid
var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
if (i > 0) {
var nodeid = eventData.substring(i + 30, i + 30 + 64);
if (nodeid.length == 64) {
var nodekey = 'node/' + domain.id + '/' + nodeid;
// See if this node exists in the database
obj.db.Get(nodekey, function (err, nodes) {
if (nodes.length == 1) {
// Yes, the node exists, compute Intel AMT digest password
var node = nodes[0];
var amtpass = obj.crypto.createHash('sha384').update(auth.username.toLowerCase() + ':' + nodeid + ":" + obj.parent.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
// Check the MD5 hash
if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
// This is an authenticated Intel AMT event, update the host address
var amthost = req.clientIp;
if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
if (node.host != amthost) {
// Get the mesh for this device
var mesh = obj.meshes[node.meshid];
if (mesh) {
// Update the database
var oldname = node.host;
node.host = amthost;
obj.db.Set(obj.cleanDevice(node));
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Intel(R) AMT host change ' + node.name + ' from group ' + mesh.name + ': ' + oldname + ' to ' + amthost };
// Remove the Intel AMT password before eventing this.
event.node = node;
if (event.node.intelamt && event.node.intelamt.pass) {
event.node = Object.assign({}, event.node); // Shallow clone
event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
delete event.node.intelamt.pass;
}
if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
obj.parent.DispatchEvent(['*', node.meshid], obj, event);
}
}
if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
//res.send('OK');
return;
}
}
});
}
}
});
}
}
}
} catch (e) { console.log(e); }
// Send authentication response
obj.crypto.randomBytes(48, function (err, buf) {
var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
res.sendStatus(401);
});
};
// Handle a server backup request
function handleBackupRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
var user = obj.users[req.session.userid];
if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
// Require modules
const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
// Good practice to catch this error explicitly
archive.on('error', function (err) { throw err; });
// Set the archive name
res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
// Pipe archive data to the file
archive.pipe(res);
// Append files from a glob pattern
archive.directory(obj.parent.datapath, false);
// Finalize the archive (ie we are done appending files but streams have to finish yet)
archive.finalize();
}
// Handle a server restore request
function handleRestoreRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
var authUserid = null;
if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
const multiparty = require('multiparty');
const form = new multiparty.Form();
form.parse(req, function (err, fields, files) {
// If an authentication cookie is embedded in the form, use that.
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
}
if (authUserid == null) { res.sendStatus(401); return; }
// Get the user
const user = obj.users[req.session.userid];
if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
res.set('Content-Type', 'text/html');
res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
parent.Stop(files.datafile[0].path);
});
}
// Handle a request to download a mesh agent
obj.handleMeshAgentRequest = function (req, res) {
var domain = getDomain(req, res);
if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
// If required, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
if ((req.query.meshinstall != null) && (req.query.id != null)) {
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
// Send meshagent with included self installer for a specific platform back
// Start by getting the .msh for this request
var meshsettings = getMshFromRequest(req, res, domain);
if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
// Get the interactive install script, this only works for non-Windows agents
var agentid = parseInt(req.query.meshinstall);
var argentInfo = obj.parent.meshAgentBinaries[agentid];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
var scriptInfo = obj.parent.meshAgentInstallScripts[6];
if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
// Change the .msh file into JSON format and merge it into the install script
var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
// Get the agent filename
var meshagentFilename = 'meshagent';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
res.statusCode = 200;
obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
} else if (req.query.id != null) {
// Send a specific mesh agent back
var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
// Download PDB debug files, only allowed for administrator or accounts with agent dump access
if (req.query.pdb == 1) {
if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
var user = obj.users[req.session.userid];
if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
if (argentInfo.id == 3) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
return;
}
if (argentInfo.id == 4) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
return;
}
}
try { res.sendStatus(404); } catch (ex) { }
return;
}
if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
// Get the agent filename
var meshagentFilename = argentInfo.rname;
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
if (argentInfo.rname.endsWith('.apk') && !meshagentFilename.endsWith('.apk')) { meshagentFilename = meshagentFilename + '.apk'; }
if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
if (req.query.zip == 1) { if (argentInfo.zdata != null) { setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.zip', null, 'meshagent.zip'); res.send(argentInfo.zdata); } else { try { res.sendStatus(404); } catch (ex) { } } return; } // Send compressed agent
setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
return;
} else {
// Check if the meshid is a time limited, encrypted cookie
var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
// We are going to embed the .msh file into the Windows executable (signed or not).
// First, fetch the mesh object to build the .msh file
var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
// If required, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
}
var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
// Prepare a mesh agent file name using the device group name.
var meshfilename = mesh.name
meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
// Customize the mesh agent file name
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
}
// Get the agent connection server name
var serverName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
// Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var meshsettings = '';
if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
}
if (req.query.id == '10006') { // Assistant settings and customizations
if ((req.query.ac != null)) { meshsettings += 'AutoConnect=' + req.query.ac + '\r\n'; } // Set MeshCentral Assistant flags if needed. 0x01 = Always Connected, 0x02 = Not System Tray
if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
if ((domain.assistantnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
if (typeof domain.assistantcustomization.title == 'string') { meshsettings += 'Title=' + domain.assistantcustomization.title + '\r\n'; }
if (typeof domain.assistantcustomization.image == 'string') {
try { meshsettings += 'Image=' + Buffer.from(obj.fs.readFileSync(parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64') + '\r\n'; } catch (ex) { console.log(ex); }
}
if (req.query.ac != '4') {
// Send with custom filename followed by device group name
if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = meshfilename.split('MeshCentralAssistant').join(domain.assistantcustomization.filename); }
} else {
// Send with custom filename, no device group name
if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = domain.assistantcustomization.filename + '.exe'; } else { meshfilename = 'MeshCentralAssistant.exe'; }
}
}
} else { // Add agent customization, not for Assistant
if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
if (domain.agentcustomization != null) {
if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
}
if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; } // Translation strings, not for MeshCentral Assistant
}
setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) {
obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: domain.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: domain.meshAgentBinaries[req.query.id].pe });
} else {
obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: obj.parent.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: obj.parent.meshAgentBinaries[req.query.id].pe });
}
return;
}
} else if (req.query.script != null) {
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
// Send a specific mesh install script back
var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
if (scriptInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
var data = scriptInfo.data;
var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
if (obj.isTrustedCert(domain) != true) {
cmdoptions.wgetoptionshttps += '--no-check-certificate ';
cmdoptions.curloptionshttps += '-k ';
}
if (domain.agentnoproxy === true) {
cmdoptions.wgetoptionshttp += '--no-proxy ';
cmdoptions.wgetoptionshttps += '--no-proxy ';
cmdoptions.curloptionshttp += '--noproxy \'*\' ';
cmdoptions.curloptionshttps += '--noproxy \'*\' ';
}
for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
res.send(data);
return;
} else if (req.query.meshcmd != null) {
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
// Send meshcmd for a specific platform back
var agentid = parseInt(req.query.meshcmd);
// If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
if ((agentid == 3) && (obj.parent.meshAgentBinaries[11000] != null)) { // Signed Windows MeshCmd.exe x86-32
var stats = null, meshCmdPath = obj.parent.meshAgentBinaries[11000].path;
try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
if ((stats != null)) {
setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
res.sendFile(meshCmdPath); return;
}
} else if ((agentid == 4) && (obj.parent.meshAgentBinaries[11001] != null)) { // Signed Windows MeshCmd64.exe x86-64
var stats = null, meshCmd64Path = obj.parent.meshAgentBinaries[11001].path;
try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
if ((stats != null)) {
setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
res.sendFile(meshCmd64Path); return;
}
} else if ((agentid == 43) && (obj.parent.meshAgentBinaries[11002] != null)) { // Signed Windows MeshCmd64.exe ARM-64
var stats = null, meshCmdAMR64Path = obj.parent.meshAgentBinaries[11002].path;
try { stats = obj.fs.statSync(meshCmdAMR64Path); } catch (e) { }
if ((stats != null)) {
setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd-arm64.exe', null, 'meshcmd');
res.sendFile(meshCmdAMR64Path); return;
}
}
// No signed agents, we are going to merge a new MeshCmd.
if (((agentid == 3) || (agentid == 4)) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
var argentInfo = obj.parent.meshAgentBinaries[agentid];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
res.statusCode = 200;
if (argentInfo.signedMeshCmdPath != null) {
// If we have a pre-signed MeshCmd, send that.
res.sendFile(argentInfo.signedMeshCmdPath);
} else {
// Merge JavaScript to a unsigned agent and send that.
obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(obj.parent.defaultMeshCmd, 'utf8'), peinfo: argentInfo.pe });
}
return;
} else if (req.query.meshaction != null) {
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
var user = obj.users[req.session.userid];
if (user == null) {
// Check if we have an authentication cookie
var c = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey);
if (c == null) { try { res.sendStatus(404); } catch (ex) { } return; }
// Download tools using a cookie
if (c.download == req.query.meshaction) {
if (req.query.meshaction == 'winrouter') {
var p = null;
if (obj.meshToolsBinaries['MeshCentralRouter']) { p = obj.meshToolsBinaries['MeshCentralRouter'].path; }
if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
} else if (req.query.meshaction == 'winassistant') {
var p = null;
if (obj.meshToolsBinaries['MeshCentralAssistant']) { p = obj.meshToolsBinaries['MeshCentralAssistant'].path; }
if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
} else if (req.query.meshaction == 'macrouter') {
var p = null;
if (obj.meshToolsBinaries['MeshCentralRouterMacOS']) { p = obj.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
}
return;
}
// Check if the cookie authenticates a user
if (c.userid == null) { try { res.sendStatus(404); } catch (ex) { } return; }
user = obj.users[c.userid];
if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
}
if ((req.query.meshaction == 'route') && (req.query.nodeid != null)) {
var nodeIdSplit = req.query.nodeid.split('/');
if ((nodeIdSplit[0] != 'node') || (nodeIdSplit[1] != domain.id)) { try { res.sendStatus(401); } catch (ex) { } return; }
obj.db.Get(req.query.nodeid, function (err, nodes) {
if ((err != null) || (nodes.length != 1)) { try { res.sendStatus(401); } catch (ex) { } return; }
var node = nodes[0];
// Create the meshaction.txt file for meshcmd.exe
var meshaction = {
action: req.query.meshaction,
localPort: 1234,
remoteName: node.name,
remoteNodeId: node._id,
remoteTarget: null,
remotePort: 3389,
username: '',
password: '',
serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
debugLevel: 0
};
if (user != null) { meshaction.username = user.name; }
if (req.query.key != null) { meshaction.loginKey = req.query.key; }
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
res.send(JSON.stringify(meshaction, null, ' '));
return;
});
} else if (req.query.meshaction == 'generic') {
var meshaction = {
username: user.name,
password: '',
serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
debugLevel: 0
};
if (user != null) { meshaction.username = user.name; }
if (req.query.key != null) { meshaction.loginKey = req.query.key; }
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
res.send(JSON.stringify(meshaction, null, ' '));
return;
} else if (req.query.meshaction == 'winrouter') {
var p = null;
if (parent.meshToolsBinaries['MeshCentralRouter']) { p = parent.meshToolsBinaries['MeshCentralRouter'].path; }
if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
} else if (req.query.meshaction == 'winassistant') {
var p = null;
if (parent.meshToolsBinaries['MeshCentralAssistant']) { p = parent.meshToolsBinaries['MeshCentralAssistant'].path; }
if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
} else if (req.query.meshaction == 'macrouter') {
var p = null;
if (parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
if (obj.fs.existsSync(p)) {
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
try { res.sendFile(p); } catch (ex) { }
} else { try { res.sendStatus(404); } catch (ex) { } }
return;
} else {
try { res.sendStatus(401); } catch (ex) { }
return;
}
} else {
domain = checkUserIpAddress(req, res); // Recheck the domain to apply user IP filtering.
if (domain == null) return;
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
var user = null, coreDumpsAllowed = false;
if (typeof req.session.userid == 'string') { user = obj.users[req.session.userid]; }
if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
// Check if this user has access to agent core dumps
if ((obj.parent.config.settings.agentcoredump === true) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
coreDumpsAllowed = true;
if ((req.query.dldump != null) && obj.common.IsFilenameValid(req.query.dldump)) {
// Download a dump file
var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
if (obj.fs.existsSync(dumpFile)) {
setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
res.sendFile(dumpFile); return;
} else {
try { res.sendStatus(404); } catch (ex) { } return;
}
}
if ((req.query.deldump != null) && obj.common.IsFilenameValid(req.query.deldump)) {
// Delete a dump file
try { obj.fs.unlinkSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.deldump)); } catch (ex) { console.log(ex); }
}
if ((req.query.dumps != null) || (req.query.deldump != null)) {
// Send list of agent core dumps
var response = '<html><head><title>Mesh Agents Core Dumps</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
response += '<tr style="background-color:lightgray"><th>ID</th><th>Upload Date</th><th>Description</th><th>Current</th><th>Dump</th><th>Size</th><th>Agent</th><th>Agent SHA384</th><th>NodeID</th><th></th></tr>';
var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
if (obj.fs.existsSync(coreDumpPath)) {
var files = obj.fs.readdirSync(coreDumpPath);
var coredumps = [];
for (var i in files) {
var file = files[i];
if (file.endsWith('.dmp')) {
var fileSplit = file.substring(0, file.length - 4).split('-');
if (fileSplit.length == 3) {
var agentid = parseInt(fileSplit[0]);
if ((isNaN(agentid) == false) && (obj.parent.meshAgentBinaries[agentid] != null)) {
var agentinfo = obj.parent.meshAgentBinaries[agentid];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
var filestats = obj.fs.statSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', file));
coredumps.push({
fileSplit: fileSplit,
agentinfo: agentinfo,
filestats: filestats,
currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()),
downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''),
deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''),
agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : ''),
time: new Date(filestats.ctime)
});
}
}
}
}
coredumps.sort(function (a, b) { if (a.time > b.time) return -1; if (a.time < b.time) return 1; return 0; });
for (var i in coredumps) {
var d = coredumps[i];
response += '<tr><td>' + d.agentinfo.id + '</td><td>' + d.time.toDateString().split(' ').join('&nbsp;') + '</td><td>' + d.agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
response += '<td style=text-align:center>' + d.currentAgent + '</td><td><a download href="' + d.downloadUrl + '">Download</a></td><td style=text-align:right>' + d.filestats.size + '</td>';
if (d.currentAgent) { response += '<td><a download href="' + d.agentUrl + '">Download</a></td>'; } else { response += '<td></td>'; }
response += '<td>' + d.fileSplit[1].toLowerCase() + '</td><td>' + d.fileSplit[2] + '</td><td><a href="' + d.deleteUrl + '">Delete</a></td></tr>';
}
}
response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + req.query.key) : '') + '">Mesh Agents</a></body></html>';
res.send(response);
return;
}
}
if (req.query.cores != null) {
// Send list of agent cores
var response = '<html><head><title>Mesh Agents Cores</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
response += '<tr style="background-color:lightgray"><th>Name</th><th>Size</th><th>Comp</th><th>Decompressed Hash SHA384</th></tr>';
for (var i in parent.defaultMeshCores) {
response += '<tr><td>' + i.split(' ').join('&nbsp;') + '</td><td style="text-align:right"><a download href="/meshagents?dlcore=' + i + '">' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + req.query.key) : '') + '</a></td><td style="text-align:right"><a download href="/meshagents?dlccore=' + i + (req.query.key ? ('?key=' + req.query.key) : '') + '">' + parent.defaultMeshCoresDeflate[i].length + '</a></td><td>' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '</td></tr>';
}
response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + req.query.key) : '') + '">Mesh Agents</a></body></html>';
res.send(response);
return;
}
if (req.query.dlcore != null) {
// Download mesh core
var bin = parent.defaultMeshCores[req.query.dlcore];
if ((bin == null) || (bin.length < 5)) { try { res.sendStatus(404); } catch (ex) { } return; }
setContentDispositionHeader(res, 'application/octet-stream', req.query.dlcore + '.js', null, 'meshcore.js');
res.send(bin.slice(4));
return;
}
if (req.query.dlccore != null) {
// Download compressed mesh core
var bin = parent.defaultMeshCoresDeflate[req.query.dlccore];
if (bin == null) { try { res.sendStatus(404); } catch (ex) { } return; }
setContentDispositionHeader(res, 'application/octet-stream', req.query.dlccore + '.js.deflate', null, 'meshcore.js.deflate');
res.send(bin);
return;
}
// Send a list of available mesh agents
var response = '<html><head><title>Mesh Agents</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
response += '<tr style="background-color:lightgray"><th>ID</th><th>Description</th><th>Link</th><th>Size</th><th>SHA384</th><th>MeshCmd</th></tr>';
var originalUrl = req.originalUrl.split('?')[0];
for (var agentid in obj.parent.meshAgentBinaries) {
if ((agentid >= 10000) && (agentid != 10005)) continue;
var agentinfo = obj.parent.meshAgentBinaries[agentid];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
response += '<td><a download href="' + originalUrl + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : '') + '">' + agentinfo.rname + '</a>';
if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) {
if ((agentid == 3) || (agentid == 4)) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&pdb=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">PDB</a>'; }
}
if (agentinfo.zdata != null) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&zip=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">ZIP</a>'; }
response += '</td>';
response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hashhex + '</td>';
response += '<td><a download href="' + originalUrl + '?meshcmd=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : '') + '">' + agentinfo.rname.replace('agent', 'cmd') + '</a></td></tr>';
}
response += '</table>';
response += '<a href="' + originalUrl + '?cores=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">MeshCores</a> ';
if (coreDumpsAllowed) { response += '<a href="' + originalUrl + '?dumps=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">MeshAgent Crash Dumps</a>'; }
response += '</body></html>';
res.send(response);
return;
}
};
// Get the web server hostname. This may change if using a domain with a DNS name.
obj.getWebServerName = function (domain, req) {
if (domain.dns != null) return domain.dns;
if ((obj.certificates.CommonName == 'un-configured') && (req != null) && (req.headers != null) && (typeof req.headers.host == 'string')) { return req.headers.host.split(':')[0]; }
return obj.certificates.CommonName;
}
// Return true if this is an allowed HTTP request origin hostname.
obj.CheckWebServerOriginName = function (domain, req) {
if (domain.allowedorigin === true) return true; // Ignore origin
if (typeof req.headers.origin != 'string') return true; // No origin in the header, this is a desktop app
const originUrl = require('url').parse(req.headers.origin, true);
if (typeof originUrl.hostname != 'string') return false; // Origin hostname is not valid
if (Array.isArray(domain.allowedorigin)) return (domain.allowedorigin.indexOf(originUrl.hostname) >= 0); // Check if this is an allowed origin from an explicit list
if (obj.isTrustedCert(domain) === false) return true; // This server does not have a trusted certificate.
if (domain.dns != null) return (domain.dns == originUrl.hostname); // Match the domain DNS
return (obj.certificates.CommonName == originUrl.hostname); // Match the default server name
}
// Create a OSX mesh agent installer
obj.handleMeshOsxAgentRequest = function (req, res) {
const domain = getDomain(req, res);
if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
if (req.query.id == null) { res.sendStatus(404); return; }
// If required, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
// Send a specific mesh agent back
var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
if ((argentInfo == null) || (req.query.meshid == null)) { res.sendStatus(404); return; }
// Check if the meshid is a time limited, encrypted cookie
var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
// We are going to embed the .msh file into the Windows executable (signed or not).
// First, fetch the mesh object to build the .msh file
var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
if (mesh == null) { res.sendStatus(401); return; }
// If required, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
}
var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
// Get the agent connection server name
var serverName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
// Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
if (domain.agentcustomization != null) { // Add agent customization
if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
}
if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
// Setup the response output
var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
archive.on('error', function (err) { throw err; });
// Customize the mesh agent file name
var meshfilename = 'MeshAgent-' + mesh.name + '.zip';
var meshexecutablename = 'meshagent';
var meshmpkgname = 'MeshAgent.mpkg';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
meshexecutablename = meshexecutablename.split('meshagent').join(domain.agentcustomization.filename);
meshmpkgname = meshmpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
}
// Customise the mesh agent display name
var meshdisplayname = 'Mesh Agent';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.displayname == 'string')) {
meshdisplayname = meshdisplayname.split('Mesh Agent').join(domain.agentcustomization.displayname);
}
// Customise the mesh agent service name
var meshservicename = 'meshagent';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.servicename == 'string')) {
meshservicename = meshservicename.split('meshagent').join(domain.agentcustomization.servicename);
}
// Customise the mesh agent company name
var meshcompanyname = 'meshagent';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.companyname == 'string')) {
meshcompanyname = meshcompanyname.split('meshagent').join(domain.agentcustomization.companyname);
}
// Set the agent download including the mesh name.
setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
archive.pipe(res);
// Opens the "MeshAgentOSXPackager.zip"
var yauzl = require('yauzl');
yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
if (err) { res.sendStatus(500); return; }
zipfile.readEntry();
zipfile.on('entry', function (entry) {
if (/\/$/.test(entry.fileName)) {
// Skip all folder entries
zipfile.readEntry();
} else {
if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
// This is a special file entry, we need to fix it.
zipfile.openReadStream(entry, function (err, readStream) {
readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
readStream.on('end', function () {
var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
var welcomemsg = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + meshname + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
archive.append(readStream.xxdata.toString().split('###DISPLAYNAME###').join(meshdisplayname).split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) });
zipfile.readEntry();
});
});
} else if (entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchAgent.plist' ||
entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchDaemon.plist' ||
entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Info.plist' ||
entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/postflight' ||
entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/Postflight.sh' ||
entry.fileName == 'MeshAgent.mpkg/Uninstall.command') {
// This is a special file entry, we need to fix it.
zipfile.openReadStream(entry, function (err, readStream) {
readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
readStream.on('end', function () {
var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
archive.append(readStream.xxdata.toString().split('###SERVICENAME###').join(meshservicename).split('###COMPANYNAME###').join(meshcompanyname).split('###EXECUTABLENAME###').join(meshexecutablename), options);
zipfile.readEntry();
});
});
} else {
// Normal file entry
zipfile.openReadStream(entry, function (err, readStream) {
if (err) { throw err; }
var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
archive.append(readStream, options);
readStream.on('end', function () { zipfile.readEntry(); });
});
}
}
});
zipfile.on('end', function () {
archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin'.replace('MeshAgent.mpkg',meshmpkgname) });
archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh'.replace('MeshAgent.mpkg',meshmpkgname) });
archive.finalize();
});
});
}
// Return a .msh file from a given request, id is the device group identifier or encrypted cookie with the identifier.
function getMshFromRequest(req, res, domain) {
// If required, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { return null; }
// Check if the meshid is a time limited, encrypted cookie
var meshcookie = obj.parent.decodeCookie(req.query.id, obj.parent.invitationLinkEncryptionKey);
if ((meshcookie != null) && (meshcookie.m != null)) { req.query.id = meshcookie.m; }
// Fetch the mesh object
var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
if (mesh == null) { return null; }
// If needed, check if this user has rights to do this
if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { return null; }
}
var meshidhex = Buffer.from(req.query.id.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
// Get the agent connection server name
var serverName = obj.getWebServerName(domain, req);
if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
// Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
if (domain.agentcustomization != null) { // Add agent customization
if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
}
if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
return meshsettings;
}
// Handle a request to download a mesh settings
obj.handleMeshSettingsRequest = function (req, res) {
const domain = getDomain(req);
if (domain == null) { return; }
//if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
var meshsettings = getMshFromRequest(req, res, domain);
if (meshsettings == null) { res.sendStatus(401); return; }
// Get the agent filename
var meshagentFilename = 'meshagent';
if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
res.send(meshsettings);
};
// Handle a request for power events
obj.handleDevicePowerEvents = function (req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (req.query.id == null) || (typeof req.query.id != 'string')) { res.sendStatus(401); return; }
var x = req.query.id.split('/');
var user = obj.users[req.session.userid];
if ((x.length != 3) || (x[0] != 'node') || (x[1] != domain.id) || (user == null) || (user.links == null)) { res.sendStatus(401); return; }
obj.db.Get(req.query.id, function (err, docs) {
if (docs.length != 1) {
res.sendStatus(401);
} else {
var node = docs[0];
// Check if we have right to this node
if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
// See how we will convert UTC time to local time
var localTimeOffset = 0;
var timeConversionSystem = 0;
if ((req.query.l != null) && (req.query.tz != null)) {
timeConversionSystem = 1;
} else if (req.query.tf != null) {
// Get local time offset (bad way)
timeConversionSystem = 2;
localTimeOffset = parseInt(req.query.tf);
if (isNaN(localTimeOffset)) { localTimeOffset = 0; }
}
// Get the list of power events and send them
setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
obj.db.getPowerTimeline(node._id, function (err, docs) {
var xevents = ['UTC Time, Local Time, State, Previous State'], prevState = 0;
for (var i in docs) {
if (docs[i].power != prevState) {
var timedoc = docs[i].time;
if (typeof timedoc == 'string') {
timedoc = new Date(timedoc);
}
prevState = docs[i].power;
var localTime = '';
if (timeConversionSystem == 1) { // Good way
localTime = new Date(timedoc.getTime()).toLocaleString(req.query.l, { timeZone: req.query.tz })
} else if (timeConversionSystem == 2) { // Bad way
localTime = new Date(timedoc.getTime() + (localTimeOffset * 60000)).toISOString();
localTime = localTime.substring(0, localTime.length - 1);
}
if (docs[i].oldPower != null) {
xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power + ',' + docs[i].oldPower);
} else {
xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power);
}
}
}
res.send(xevents.join('\r\n'));
});
}
});
}
if (parent.pluginHandler != null) {
// Handle a plugin admin request
obj.handlePluginAdminReq = function (req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
var user = obj.users[req.session.userid];
if (user == null) { res.sendStatus(401); return; }
parent.pluginHandler.handleAdminReq(req, res, user, obj);
}
obj.handlePluginAdminPostReq = function (req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
var user = obj.users[req.session.userid];
if (user == null) { res.sendStatus(401); return; }
parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
}
obj.handlePluginJS = function (req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
var user = obj.users[req.session.userid];
if (user == null) { res.sendStatus(401); return; }
parent.pluginHandler.refreshJS(req, res);
}
}
// Starts the HTTPS server, this should be called after the user/mesh tables are loaded
function serverStart() {
// Start the server, only after users and meshes are loaded from the database.
if (obj.args.tlsoffload) {
// Setup the HTTP server without TLS
obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
} else {
var ciphers = [
'TLS_AES_256_GCM_SHA384',
'TLS_AES_128_GCM_SHA256',
'TLS_AES_128_CCM_8_SHA256',
'TLS_AES_128_CCM_SHA256',
'TLS_CHACHA20_POLY1305_SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'DHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-CHACHA20-POLY1305', // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
'ECDHE-ARIA128-GCM-SHA256',
'ECDHE-ARIA256-GCM-SHA384',
'ECDHE-RSA-AES128-SHA256', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
'ECDHE-RSA-AES256-SHA384', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!RC4',
'!MD5',
'!PSK',
'!SRP',
'!CAMELLIA'
].join(':');
if (obj.useNodeDefaultTLSCiphers) {
ciphers = require("tls").DEFAULT_CIPHERS;
}
if (obj.tlsCiphers) {
ciphers = obj.tlsCiphers;
if (Array.isArray(obj.tlsCiphers)) {
ciphers = obj.tlsCiphers.join(":");
}
}
// Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
//const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:!aNULL:!eNULL:!EXPORT:!RSA:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; // This does not work with TLS 1.3
const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: ciphers, secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
if (obj.tlsSniCredentials != null) { tlsOptions.SNICallback = TlsSniCallback; } // We have multiple web server certificate used depending on the domain name
obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
//obj.tlsServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
}
// Start a second agent-only server if needed
if (obj.args.agentport) {
var agentPortTls = true;
if (obj.args.tlsoffload != null) { agentPortTls = false; }
if (typeof obj.args.agentporttls == 'boolean') { agentPortTls = obj.args.agentporttls; }
if (obj.certificates.webdefault == null) { agentPortTls = false; }
if (agentPortTls == false) {
// Setup the HTTP server without TLS
obj.expressWsAlt = require('express-ws')(obj.agentapp, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
} else {
// Setup the agent HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
// If TLS is used on the agent port, we always use the default TLS certificate.
const tlsOptions = { cert: obj.certificates.webdefault.cert, key: obj.certificates.webdefault.key, ca: obj.certificates.webdefault.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
obj.tlsAltServer = require('https').createServer(tlsOptions, obj.agentapp);
obj.tlsAltServer.on('secureConnection', function () { /*console.log('tlsAltServer secureConnection');*/ });
obj.tlsAltServer.on('error', function (err) { console.log('tlsAltServer error', err); });
//obj.tlsAltServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
obj.tlsAltServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
}
}
// Setup middleware
obj.app.engine('handlebars', obj.exphbs({ defaultLayout: false }));
obj.app.set('view engine', 'handlebars');
if (obj.args.trustedproxy) {
// Reverse proxy should add the "X-Forwarded-*" headers
try {
obj.app.set('trust proxy', obj.args.trustedproxy);
} catch (ex) {
// If there is an error, try to resolve the string
if ((obj.args.trustedproxy.length == 1) && (typeof obj.args.trustedproxy[0] == 'string')) {
require('dns').lookup(obj.args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.trustedproxy = [address]; } });
}
}
}
else if (typeof obj.args.tlsoffload == 'object') {
// Reverse proxy should add the "X-Forwarded-*" headers
try {
obj.app.set('trust proxy', obj.args.tlsoffload);
} catch (ex) {
// If there is an error, try to resolve the string
if ((Array.isArray(obj.args.tlsoffload)) && (obj.args.tlsoffload.length == 1) && (typeof obj.args.tlsoffload[0] == 'string')) {
require('dns').lookup(obj.args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.tlsoffload = [address]; } });
}
}
}
// Setup a keygrip instance with higher default security, default hash is SHA1, we want to bump that up with SHA384
// If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
// If args.sessionkey is a string, use it as a single key, but args.sessionkey can also be used as an array of keys.
const keygrip = require('keygrip')((typeof obj.args.sessionkey == 'string') ? [obj.args.sessionkey] : obj.args.sessionkey, 'sha384', 'base64');
// Setup the cookie session
const sessionOptions = {
name: 'xid', // Recommended security practice to not use the default cookie name
httpOnly: true,
keys: keygrip,
secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax')
}
if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60000); } // sessiontime is minutes
obj.app.use(require('cookie-session')(sessionOptions));
obj.app.use(function (request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
if (request.session && !request.session.regenerate) {
request.session.regenerate = function (cb) {
cb()
}
}
if (request.session && !request.session.save) {
request.session.save = function (cb) {
cb()
}
}
next()
});
// Handle all incoming web sockets, see if some need to be handled as web relays
obj.app.ws('/*', function (ws, req, next) {
if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) { handleWebRelayWebSocket(ws, req); return; }
return next();
});
// Add HTTP security headers to all responses
obj.app.use(async function (req, res, next) {
// Check if a session is destroyed
if (typeof req.session.userid == 'string') {
if (typeof req.session.x == 'string') {
if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) {
delete req.session.userid;
delete req.session.ip;
delete req.session.t;
delete req.session.x;
}
} else {
// Legacy session without a random, add one.
setSessionRandom(req);
}
}
// Remove legacy values from the session to keep the session as small as possible
delete req.session.u2f;
delete req.session.domainid;
delete req.session.nowInMinutes;
delete req.session.tokenuserid;
delete req.session.tokenusername;
delete req.session.tokenpassword;
delete req.session.tokenemail;
delete req.session.tokensms;
delete req.session.tokenpush;
delete req.session.tusername;
delete req.session.tpassword;
// Useful for debugging reverse proxy issues
parent.debug('httpheaders', req.method, req.url, req.headers);
// If this request came over HTTP, redirect to HTTPS
if (req.headers['x-forwarded-proto'] == 'http') {
var host = req.headers.host;
if (typeof host == 'string') { host = host.split(':')[0]; }
if ((host == null) && (obj.certificates != null)) { host = obj.certificates.CommonName; if (obj.certificates.CommonName.indexOf('.') == -1) { host = req.headers.host; } }
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
res.redirect('https://' + host + ':' + httpsPort + req.url);
return;
}
// Perform traffic accounting
if (req.headers.upgrade == 'websocket') {
// We don't count traffic on WebSockets since it's counted by the handling modules.
obj.trafficStats.httpWebSocketCount++;
} else {
// Normal HTTP traffic is counted
obj.trafficStats.httpRequestCount++;
if (typeof req.socket.xbytesRead != 'number') {
req.socket.xbytesRead = 0;
req.socket.xbytesWritten = 0;
req.socket.on('close', function () {
// Perform final accounting
obj.trafficStats.httpIn += (this.bytesRead - this.xbytesRead);
obj.trafficStats.httpOut += (this.bytesWritten - this.xbytesWritten);
this.xbytesRead = this.bytesRead;
this.xbytesWritten = this.bytesWritten;
});
} else {
// Update counters
obj.trafficStats.httpIn += (req.socket.bytesRead - req.socket.xbytesRead);
obj.trafficStats.httpOut += (req.socket.bytesWritten - req.socket.xbytesWritten);
req.socket.xbytesRead = req.socket.bytesRead;
req.socket.xbytesWritten = req.socket.bytesWritten;
}
}
// Set the real IP address of the request
// If a trusted reverse-proxy is sending us the remote IP address, use it.
var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
if (
(obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
) {
// Get client IP
if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
} else if (req.headers['x-forwarded-for']) {
req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
} else if (req.headers['x-real-ip']) {
req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
} else {
req.clientIp = ipex;
}
// If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
const clientIpSplit = req.clientIp.split(':');
if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
// Get server host
if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host'].split(',')[0]; } // If multiple hosts are specified with a comma, take the first one.
} else {
req.clientIp = ipex;
}
// If this is a web relay connection, handle it here.
if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) {
if (['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; }
}
// Get the domain for this request
const domain = req.xdomain = getDomain(req);
parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
// Skip the rest if this is an agent connection
if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
// Setup security headers
const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
var selfurl = ' wss://' + req.headers.host;
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
// If the web relay port is enabled, allow the web page to redirect to it
var extraFrameSrc = '';
if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
}
// Finish setup security headers
const headers = {
'Referrer-Policy': 'no-referrer',
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' blob: mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self'; manifest-src 'self'"
};
if (req.headers['user-agent'] && (req.headers['user-agent'].indexOf('Chrome') >= 0)) { headers['Permissions-Policy'] = 'interest-cohort=()'; } // Remove Google's FLoC Network, only send this if Chrome browser
if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
if ((parent.config.settings.stricttransportsecurity === true) || ((parent.config.settings.stricttransportsecurity !== false) && (obj.isTrustedCert(domain)))) { if (typeof parent.config.settings.stricttransportsecurity == 'string') { headers['Strict-Transport-Security'] = parent.config.settings.stricttransportsecurity; } else { headers['Strict-Transport-Security'] = 'max-age=63072000'; } }
// If this domain has configured headers, add them. If a header is set to null, remove it.
if ((domain != null) && (domain.httpheaders != null) && (typeof domain.httpheaders == 'object')) {
for (var i in domain.httpheaders) { if (domain.httpheaders[i] === null) { delete headers[i]; } else { headers[i] = domain.httpheaders[i]; } }
}
res.set(headers);
// Check the session if bound to the external IP address
if ((req.session.ip != null) && (req.clientIp != null) && !checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
// Extend the session time by forcing a change to the session every minute.
if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
// Check CrowdSec Bounser if configured
if ((parent.crowdSecBounser != null) && (req.headers['upgrade'] != 'websocket') && (req.session.userid == null)) { if ((await parent.crowdSecBounser.process(domain, req, res, next)) == true) { return; } }
// Debugging code, this will stop the agent from crashing if two responses are made to the same request.
const render = res.render;
const send = res.send;
res.render = function renderWrapper(...args) {
Error.captureStackTrace(this);
return render.apply(this, args);
};
res.send = function sendWrapper(...args) {
try {
send.apply(this, args);
} catch (err) {
console.error(`Error in res.send | ${err.code} | ${err.message} | ${res.stack}`);
try {
var errlogpath = null;
if (typeof parent.args.mesherrorlogpath == 'string') { errlogpath = parent.path.join(parent.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = parent.getConfigFilePath('mesherrors.txt'); }
parent.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + `Error in res.send | ${err.code} | ${err.message} | ${res.stack}` + '\r\n');
} catch (ex) { parent.debug('error', 'Unable to write to mesherrors.txt.'); }
}
};
// Continue processing the request
return next();
});
if (obj.agentapp) {
// Add HTTP security headers to all responses
obj.agentapp.use(function (req, res, next) {
// Set the real IP address of the request
// If a trusted reverse-proxy is sending us the remote IP address, use it.
var ipex = '0.0.0.0';
if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
if (
(obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
) {
if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
} else if (req.headers['x-forwarded-for']) {
req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
} else if (req.headers['x-real-ip']) {
req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
} else {
req.clientIp = ipex;
}
} else {
req.clientIp = ipex;
}
// Get the domain for this request
const domain = req.xdomain = getDomain(req);
parent.debug('webrequest', '(' + req.clientIp + ') AgentPort: ' + req.url);
res.removeHeader('X-Powered-By');
return next();
});
}
// Setup all sharing domains and check if auth strategies need setup
var setupSSO = false
for (var i in parent.config.domains) {
if ((parent.config.domains[i].dns == null) && (parent.config.domains[i].share != null)) { obj.app.use(parent.config.domains[i].url, obj.express.static(parent.config.domains[i].share)); }
if (typeof parent.config.domains[i].authstrategies == 'object') { setupSSO = true };
}
if (setupSSO) {
setupAllDomainAuthStrategies().then(() => finalizeWebserver());
} else {
finalizeWebserver()
}
// Setup all domain auth strategy passport.js
async function setupAllDomainAuthStrategies() {
for (var i in parent.config.domains) {
if (parent.config.domains[i].dns != null) {
if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { 'authStrategyFlags': 0 }; }
parent.config.domains[''].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains['']);
} else {
if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { 'authStrategyFlags': 0 }; }
parent.config.domains[i].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
}
}
}
function setupHTTPHandlers() {
// Setup all HTTP handlers
if (parent.pluginHandler != null) {
parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
}
if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
for (var i in parent.config.domains) {
if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
var domain = parent.config.domains[i];
var url = domain.url;
if (typeof domain.rootredirect == 'string') {
// Root page redirects the user to a different URL
obj.app.get(url, handleRootRedirect);
} else {
// Present the login page as the root page
obj.app.get(url, handleRootRequest);
obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
}
obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
obj.app.get(url + 'terms', handleTermsRequest);
obj.app.get(url + 'xterm', handleXTermRequest);
obj.app.get(url + 'login', handleRootRequest);
obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
obj.app.get(url + 'logout', handleLogoutRequest);
obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
obj.app.get(url + 'checkmail', handleCheckMailRequest);
obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
obj.app.get(url + 'messenger', handleMessengerRequest);
obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
obj.app.get(url + 'commander.ashx', handleMeshCommander);
obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie, authData) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
obj.app.ws(url + 'control.ashx', function (ws, req) {
getWebsocketArgs(ws, req, function (ws, req) {
const domain = getDomain(req);
if (obj.CheckWebServerOriginName(domain, req) == false) {
try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
try { ws.close(); } catch (ex) { }
return;
}
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
if (user == null) { // User is not authenticated, perform inner server authentication
if (req.headers['x-meshauth'] === '*') {
PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); }); // User is authenticated
} else {
try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
}
} else {
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
}
});
});
});
obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
obj.app.get(url + 'logo.png', handleLogoRequest);
obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
obj.app.get(url + 'recordings.ashx', handleGetRecordings);
obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
obj.app.get(url + 'player.htm', handlePlayerRequest);
obj.app.get(url + 'player', handlePlayerRequest);
obj.app.get(url + 'sharing', handleSharingRequest);
obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
} else {
obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
}
});
});
if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
if ((user == null) || (cookie == null)) {
try { ws1.close(); } catch (ex) { }
} else {
obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
}
});
});
}
if (domain.agentinvitecodes == true) {
obj.app.get(url + 'invite', handleInviteRequest);
obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
}
if (parent.pluginHandler != null) {
obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
}
// New account CAPTCHA request
if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
}
// Check CrowdSec Bounser if configured
if (parent.crowdSecBounser != null) {
obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
}
// Setup IP-KVM relay if supported
if (domain.ipkvm) {
obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
});
obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
const domain = getDomain(req);
if (domain == null) return;
parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
});
}
// Setup RDP unless indicated as disabled
if (domain.mstsc !== false) {
obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
// If no user is logged in and we have a default user, set it now.
if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
});
}
// Setup SSH if needed
if (domain.ssh === true) {
obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
const domain = getDomain(req);
if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
// If no user is logged in and we have a default user, set it now.
if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
});
obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
});
});
obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
});
});
}
// Setup firebase push only server
if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
}
// Setup auth strategies using passport if needed
if (typeof domain.authstrategies == 'object') {
parent.authLog('setupHTTPHandlers', `Setting up authentication strategies login and callback URLs for ${domain.id == '' ? 'root' : '"' + domain.id + '"'} domain.`);
// Twitter
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
obj.app.get(url + 'auth-twitter', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
});
obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
// This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
var url = req.url;
if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
res.set('Content-Type', 'text/html');
res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
} else {
domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
}
}, handleStrategyLogin);
}
// Google
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
obj.app.get(url + 'auth-google', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
});
obj.app.get(url + 'auth-google-callback', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('google-' + domain.id, { failureRedirect: '/' })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
}, handleStrategyLogin);
}
// GitHub
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
obj.app.get(url + 'auth-github', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
});
obj.app.get(url + 'auth-github-callback', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('github-' + domain.id, { failureRedirect: '/' })(req, res, next);
}, handleStrategyLogin);
}
// Azure
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
obj.app.get(url + 'auth-azure', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
});
obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
// This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
var url = req.url;
if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
res.set('Content-Type', 'text/html');
res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
} else {
if (req.query.state != null) {
var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: '/' })(req, res, next); return; }
}
next();
}
}, handleStrategyLogin);
}
// Setup OpenID Connect URLs
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
let authURL = url + 'auth-oidc'
parent.authLog('setupHTTPHandlers', `OIDC: Authorization URL: ${authURL}`);
obj.app.get(authURL, function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
let redirectPath
if (typeof domain.authstrategies.oidc.client.redirect_uri == 'string') {
redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uri)).pathname
} else if (Array.isArray(domain.authstrategies.oidc.client.redirect_uris)) {
redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uris[0])).pathname
} else {
redirectPath = url + 'auth-oidc-callback'
}
parent.authLog('setupHTTPHandlers', `OIDC: Callback URL: ${redirectPath}`);
obj.app.get(redirectPath, obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: '/', failureFlash: true })(req, res, next);
}, handleStrategyLogin);
}
// Generic SAML
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
obj.app.get(url + 'auth-saml', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
}, handleStrategyLogin);
}
// Intel SAML
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
obj.app.get(url + 'auth-intel', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
}, handleStrategyLogin);
}
// JumpCloud SAML
if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
}, handleStrategyLogin);
}
}
// Server redirects
if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
// Server picture
obj.app.get(url + 'serverpic.ashx', function (req, res) {
// Check if we have "server.jpg" in the data folder, if so, use that.
if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
res.set({ 'Content-Type': 'image/png' });
res.send(parent.configurationFiles['server.png']);
} else {
// Check if we have "server.jpg" in the data folder, if so, use that.
var p = obj.path.join(obj.parent.datapath, 'server.png');
if (obj.fs.existsSync(p)) {
// Use the data folder server picture
try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
} else {
var domain = getDomain(req);
if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
// Use the domain server picture
try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
} else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
// Use the override server picture
try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
} else {
// Use the default server picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
}
}
}
});
// Receive mesh agent connections
obj.app.ws(url + 'agent.ashx', function (ws, req) {
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
//console.log('Agent connect: ' + req.clientIp);
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
});
// Setup MQTT broker over websocket
if (obj.parent.mqttbroker != null) {
obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
var serialtunnel = SerialTunnel();
serialtunnel.xtransport = 'ws';
serialtunnel.xdomain = domain;
serialtunnel.xip = req.clientIp;
ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
ws.on('close', function () { serialtunnel.emit('end'); });
obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
});
}
// Setup any .well-known folders
var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
// Setup the alternative agent-only port
if (obj.agentapp) {
// Receive mesh agent connections on alternate port
obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
});
// Setup mesh relay on alternative agent-only port
obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
} else {
obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
}
});
});
// Allows agents to transfer files
obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
// Setup agent to/from server file transfer handler
obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
// Setup agent downloads for meshcore updates
obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
}
// Setup web relay on this web server if needed
// We set this up when a DNS name is used as a web relay instead of a port
if (obj.args.relaydns != null) {
obj.webRelayRouter = require('express').Router();
// This is the magic URL that will setup the relay session
obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
res.set({ 'Cache-Control': 'no-store' });
parent.debug('web', 'webRelaySetup');
// Decode the relay cookie
if (req.query.c == null) { res.sendStatus(404); return; }
// Decode and check if this relay cookie is valid
var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey, 32); // Allow cookies up to 32 minutes old. The web page will renew this cookie every 30 minutes.
if (urlCookie == null) { res.sendStatus(404); return; }
// Decode the incoming cookie
if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
// This is a standard user, figure out what our web relay will be.
if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
userid = req.session.userid;
domainid = userid.split('/')[1];
domain = parent.config.domains[domainid];
nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
port = parseInt(req.query.p);
appid = parseInt(req.query.appid);
webSessionId = req.session.userid + '/' + req.session.x;
// Check that all the required arguments are present
if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
} else if (urlCookie.r == 8) {
// This is a guest user, figure out what our web relay will be.
userid = urlCookie.userid;
domainid = userid.split('/')[1];
domain = parent.config.domains[domainid];
nodeid = urlCookie.nid;
addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
port = urlCookie.port;
appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
webSessionId = userid + '/' + urlCookie.pid;
publicid = urlCookie.pid;
if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
expire = urlCookie.expire;
if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
}
// No session identifier was setup, exit now
if (webSessionId == null) { res.sendStatus(404); return; }
// Check that we have an exact session on any of the relay DNS names
var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
for (var hostIndex in obj.args.relaydns) {
const host = obj.args.relaydns[hostIndex];
xrelaySessionId = webSessionId + '/' + host;
xrelaySession = webRelaySessions[xrelaySessionId];
if (xrelaySession == null) {
// We found an unused hostname, save this as it could be useful.
if (freeRelayHost == null) { freeRelayHost = host; }
} else {
// Check if we already have a relay session that matches exactly what we want
if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
// We found an exact match, we are all setup already, redirect to root of that DNS name
if (host == req.hostname) {
// Request was made on the same host, redirect to root.
res.redirect('/');
} else {
// Request was made to a different host
const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
}
return;
}
// Keep a record of the oldest web relay session, this could be useful.
if (oldestRelayHost == null) {
// Oldest host not set yet, set it
oldestRelayHost = host;
oldestRelayTime = xrelaySession.lastOperation;
} else {
// Check if this host is older then oldest so far
if (oldestRelayTime > xrelaySession.lastOperation) {
oldestRelayHost = host;
oldestRelayTime = xrelaySession.lastOperation;
}
}
}
}
// Check that the user has rights to access this device
parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
// If there is no remote control or relay rights, reject this web relay
if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
// Check if there is a free relay DNS name we can use
var selectedHost = null;
if (freeRelayHost != null) {
// There is a free one, use it.
selectedHost = freeRelayHost;
} else {
// No free ones, close the oldest one
selectedHost = oldestRelayHost;
}
xrelaySessionId = webSessionId + '/' + selectedHost;
if (selectedHost == req.hostname) {
// If this web relay session id is not free, close it now
xrelaySession = webRelaySessions[xrelaySessionId];
if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
// Create a web relay session
const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
relaySession.xpublicid = publicid;
relaySession.onclose = function (sessionId) {
// Remove the relay session
delete webRelaySessions[sessionId];
// If there are not more relay sessions, clear the cleanup timer
if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
}
// Set the multi-tunnel session
webRelaySessions[xrelaySessionId] = relaySession;
// Setup the cleanup timer if needed
if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
// Redirect to root.
res.redirect('/');
} else {
if (req.query.noredirect != null) {
// No redirects allowed, fail here. This is important to make sure there is no redirect cascades
res.sendStatus(404);
} else {
// Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
}
}
});
});
// Handle all incoming requests as web relays
obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
// Handle all incoming requests as web relays
obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
// Handle all incoming requests as web relays
obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
// Handle all incoming requests as web relays
obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
// Handle all incoming requests as web relays
obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
// Handle all incoming requests as web relays
obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
}
// Indicates to ExpressJS that the override public folder should be used to serve static files.
if (parent.config.domains[i].webpublicpath != null) {
// Use domain public path
obj.app.use(url, obj.express.static(parent.config.domains[i].webpublicpath));
} else if (obj.parent.webPublicOverridePath != null) {
// Use override path
obj.app.use(url, obj.express.static(obj.parent.webPublicOverridePath));
}
// Indicates to ExpressJS that the default public folder should be used to serve static files.
obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
// Start regular disconnection list flush every 2 minutes.
obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
}
}
function finalizeWebserver() {
// Setup all HTTP handlers
setupHTTPHandlers()
// Handle 404 error
if (obj.args.nice404 !== false) {
obj.app.use(function (req, res, next) {
parent.debug('web', '404 Error ' + req.url);
var domain = getDomain(req);
if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
const cspNonce = obj.crypto.randomBytes(15).toString('base64');
res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
res.status(404).render(getRenderPage((domain.sitestyle == 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
});
}
// Start server on a free port.
CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
// Start on a second agent-only alternative port if needed.
if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
// We are done starting the web server.
if (doneFunc) doneFunc();
}
}
// Auth strategy flags
const domainAuthStrategyConsts = {
twitter: 1,
google: 2,
github: 3,
reddit: 8, // Deprecated
azure: 16,
oidc: 32,
saml: 64,
intelSaml: 128,
jumpCloudSaml: 256
}
// Setup auth strategies for a domain
async function setupDomainAuthStrategy(domain) {
// Return binary flags representing all auth strategies that have been setup
let authStrategyFlags = 0;
// Setup auth strategies using passport if needed
if (typeof domain.authstrategies != 'object') return authStrategyFlags;
const url = domain.url
const passport = domain.passport = require('passport');
passport.serializeUser(function (user, done) { done(null, user.sid); });
passport.deserializeUser(function (sid, done) { done(null, { sid: sid }); });
obj.app.use(passport.initialize());
// Twitter
if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) {
const TwitterStrategy = require('passport-twitter');
let options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
if (typeof domain.authstrategies.twitter.callbackurl == 'string') { options.callbackURL = domain.authstrategies.twitter.callbackurl; } else { options.callbackURL = url + 'auth-twitter-callback'; }
parent.authLog('setupDomainAuthStrategy', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
passport.use('twitter-' + domain.id, new TwitterStrategy(options,
function (token, tokenSecret, profile, cb) {
parent.authLog('setupDomainAuthStrategy', 'Twitter profile: ' + JSON.stringify(profile));
var user = { sid: '~twitter:' + profile.id, name: profile.displayName, strategy: 'twitter' };
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
return cb(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.twitter;
}
// Google
if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) {
const GoogleStrategy = require('passport-google-oauth20');
let options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
if (typeof domain.authstrategies.google.callbackurl == 'string') { options.callbackURL = domain.authstrategies.google.callbackurl; } else { options.callbackURL = url + 'auth-google-callback'; }
parent.authLog('setupDomainAuthStrategy', 'Adding Google SSO with options: ' + JSON.stringify(options));
passport.use('google-' + domain.id, new GoogleStrategy(options,
function (token, tokenSecret, profile, cb) {
parent.authLog('setupDomainAuthStrategy', 'Google profile: ' + JSON.stringify(profile));
var user = { sid: '~google:' + profile.id, name: profile.displayName, strategy: 'google' };
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string') && (profile.emails[0].verified == true)) { user.email = profile.emails[0].value; }
return cb(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.google;
}
// Github
if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) {
const GitHubStrategy = require('passport-github2');
let options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
if (typeof domain.authstrategies.github.callbackurl == 'string') { options.callbackURL = domain.authstrategies.github.callbackurl; } else { options.callbackURL = url + 'auth-github-callback'; }
parent.authLog('setupDomainAuthStrategy', 'Adding Github SSO with options: ' + JSON.stringify(options));
passport.use('github-' + domain.id, new GitHubStrategy(options,
function (token, tokenSecret, profile, cb) {
parent.authLog('setupDomainAuthStrategy', 'Github profile: ' + JSON.stringify(profile));
var user = { sid: '~github:' + profile.id, name: profile.displayName, strategy: 'github' };
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
return cb(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.github;
}
// Azure
if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
const AzureOAuth2Strategy = require('passport-azure-oauth2');
let options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
if (typeof domain.authstrategies.azure.callbackurl == 'string') { options.callbackURL = domain.authstrategies.azure.callbackurl; } else { options.callbackURL = url + 'auth-azure-callback'; }
parent.authLog('setupDomainAuthStrategy', 'Adding Azure SSO with options: ' + JSON.stringify(options));
passport.use('azure-' + domain.id, new AzureOAuth2Strategy(options,
function (accessToken, refreshtoken, params, profile, done) {
var userex = null;
try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
var user = null;
if (userex != null) {
var user = { sid: '~azure:' + userex.unique_name, name: userex.name, strategy: 'azure' };
if (typeof userex.email == 'string') { user.email = userex.email; }
}
return done(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.azure;
}
// Generic SAML
if (typeof domain.authstrategies.saml == 'object') {
if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
parent.debug('error', 'Missing SAML configuration.');
} else {
const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
var cert = obj.fs.readFileSync(certPath);
if (cert == null) {
parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
} else {
var options = { entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
if (typeof domain.authstrategies.saml.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.saml.callbackurl; } else { options.callbackUrl = url + 'auth-saml-callback'; }
if (domain.authstrategies.saml.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.saml.disablerequestedauthncontext; }
if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
parent.authLog('setupDomainAuthStrategy', 'Adding SAML SSO with options: ' + JSON.stringify(options));
options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
const SamlStrategy = require('passport-saml').Strategy;
passport.use('saml-' + domain.id, new SamlStrategy(options,
function (profile, done) {
parent.authLog('setupDomainAuthStrategy', 'SAML profile: ' + JSON.stringify(profile));
if (typeof profile.nameID != 'string') { return done(); }
var user = { sid: '~saml:' + profile.nameID, name: profile.nameID, strategy: 'saml' };
if (typeof profile.displayname == 'string') {
user.name = profile.displayname;
} else if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) {
user.name = profile.firstname + ' ' + profile.lastname;
}
if (typeof profile.email == 'string') { user.email = profile.email; }
return done(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.saml
}
}
}
// Intel SAML
if (typeof domain.authstrategies.intel == 'object') {
if ((typeof domain.authstrategies.intel.cert != 'string') || (typeof domain.authstrategies.intel.idpurl != 'string')) {
parent.debug('error', 'Missing Intel SAML configuration.');
} else {
var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
if (cert == null) {
parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
} else {
var options = { entryPoint: domain.authstrategies.intel.idpurl, issuer: 'meshcentral' };
if (typeof domain.authstrategies.intel.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.intel.callbackurl; } else { options.callbackUrl = url + 'auth-intel-callback'; }
if (domain.authstrategies.intel.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.intel.disablerequestedauthncontext; }
if (typeof domain.authstrategies.intel.entityid == 'string') { options.issuer = domain.authstrategies.intel.entityid; }
parent.authLog('setupDomainAuthStrategy', 'Adding Intel SSO with options: ' + JSON.stringify(options));
options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
const SamlStrategy = require('passport-saml').Strategy;
passport.use('isaml-' + domain.id, new SamlStrategy(options,
function (profile, done) {
parent.authLog('setupDomainAuthStrategy', 'Intel profile: ' + JSON.stringify(profile));
if (typeof profile.nameID != 'string') { return done(); }
var user = { sid: '~intel:' + profile.nameID, name: profile.nameID, strategy: 'intel' };
if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
else if ((typeof profile.FirstName == 'string') && (typeof profile.LastName == 'string')) { user.name = profile.FirstName + ' ' + profile.LastName; }
if (typeof profile.email == 'string') { user.email = profile.email; }
else if (typeof profile.EmailAddress == 'string') { user.email = profile.EmailAddress; }
return done(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.intelSaml
}
}
}
// JumpCloud SAML
if (typeof domain.authstrategies.jumpcloud == 'object') {
if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
parent.debug('error', 'Missing JumpCloud SAML configuration.');
} else {
var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
if (cert == null) {
parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
} else {
var options = { entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
if (typeof domain.authstrategies.jumpcloud.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.jumpcloud.callbackurl; } else { options.callbackUrl = url + 'auth-jumpcloud-callback'; }
if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
parent.authLog('setupDomainAuthStrategy', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options));
options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
const SamlStrategy = require('passport-saml').Strategy;
passport.use('jumpcloud-' + domain.id, new SamlStrategy(options,
function (profile, done) {
parent.authLog('setupDomainAuthStrategy', 'JumpCloud profile: ' + JSON.stringify(profile));
if (typeof profile.nameID != 'string') { return done(); }
var user = { sid: '~jumpcloud:' + profile.nameID, name: profile.nameID, strategy: 'jumpcloud' };
if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
if (typeof profile.email == 'string') { user.email = profile.email; }
return done(null, user);
}
));
authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
}
}
}
// Setup OpenID Connect Authentication Strategy
if (obj.common.validateObject(domain.authstrategies.oidc)) {
parent.authLog('setupDomainAuthStrategy', `OIDC: Setting up strategy for domain: ${domain.id == null ? 'default' : domain.id}`);
// Ensure required objects exist
let initStrategy = domain.authstrategies.oidc
if (typeof initStrategy.issuer == 'string') { initStrategy.issuer = { 'issuer': initStrategy.issuer } }
let strategy = migrateOldConfigs(Object.assign({ 'client': {}, 'issuer': {}, 'options': {}, 'custom': {}, 'obj': { 'openidClient': require('openid-client') } }, initStrategy))
let preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
if (!preset) {
if (typeof strategy.custom.tenant_id == 'string') { strategy.custom.preset = preset = 'azure' }
if (strategy.custom.customer_id || strategy.custom.identitysource || strategy.client.client_id.split('.')[2] == 'googleusercontent') { strategy.custom.preset = preset = 'google' }
}
// Check issuer url
let presetIssuer
if (preset == 'azure') { presetIssuer = 'https://login.microsoftonline.com/' + strategy.custom.tenant_id + '/v2.0'; }
if (preset == 'google') { presetIssuer = 'https://accounts.google.com'; }
if (!obj.common.validateString(strategy.issuer.issuer)) {
if (!preset) {
let error = new Error('OIDC: Missing issuer URI.');
parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
throw error;
} else {
strategy.issuer.issuer = presetIssuer
parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
}
} else if ((typeof strategy.issuer.issuer == 'string') && (typeof strategy.custom.preset == 'string')) {
let error = new Error(`OIDC: PRESET: ${strategy.custom.preset.toUpperCase()}: PRESET OVERRIDDEN: CONFIG ISSUER: ${strategy.issuer.issuer} PRESET ISSUER: ${presetIssuer}`);
parent.authLog('setupDomainAuthStrategy', error.message);
console.warn(error)
}
// Setup Strategy Options
strategy.custom.scope = obj.common.convertStrArray(strategy.custom.scope, ' ')
if (strategy.custom.scope.length > 1) {
strategy.options = Object.assign(strategy.options, { 'params': { 'scope': strategy.custom.scope } })
} else {
strategy.options = Object.assign(strategy.options, { 'params': { 'scope': ['openid', 'profile', 'email'] } })
}
if (typeof strategy.groups == 'object') {
let groupScope = strategy.groups.scope || null
if (groupScope == null) {
if (preset == 'azure') { groupScope = 'Group.Read.All' }
if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
if (typeof preset != 'string') { groupScope = 'groups' }
}
strategy.options.params.scope.push(groupScope)
}
strategy.options.params.scope = strategy.options.params.scope.join(' ')
// Discover additional information if available, use endpoints from config if present
let issuer
try {
parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
} catch (err) {
let error = new Error('OIDC: Discovery failed.', { cause: err });
parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
throw error
}
if (Object.keys(strategy.issuer).length > 1) {
parent.authLog('setupDomainAuthStrategy', `OIDC: Adding Issuer Metadata: ${JSON.stringify(strategy.issuer)}`);
issuer = new strategy.obj.openidClient.Issuer(Object.assign(issuer?.metadata, strategy.issuer));
}
strategy.issuer = issuer?.metadata
strategy.obj.issuer = issuer
// Make sure redirect_uri and post_logout_redirect_uri exist before continuing
if (!strategy.client.redirect_uri) {
strategy.client.redirect_uri = 'https://' + parent.config.settings.cert + url + 'auth-oidc-callback';
}
if (!strategy.client.post_logout_redirect_uri) {
strategy.client.post_logout_redirect_uri = 'https://' + parent.config.settings.cert + url + 'login';
}
// Create client and overwrite in options
let client = new issuer.Client(strategy.client)
strategy.options = Object.assign(strategy.options, { 'client': client });
strategy.client = client.metadata
strategy.obj.client = client
// Setup strategy and save configs for later
passport.use('oidc-' + domain.id, new strategy.obj.openidClient.Strategy(strategy.options, oidcCallback));
parent.config.domains[domain.id].authstrategies.oidc = strategy;
parent.debug('verbose', 'OIDC: Saved Configuration: ' + JSON.stringify(strategy));
if (preset) { parent.authLog('setupDomainAuthStrategy', 'OIDC: ' + preset.toUpperCase() + ': Setup Complete'); }
else { parent.authLog('setupDomainAuthStrategy', 'OIDC: Setup Complete'); }
authStrategyFlags |= domainAuthStrategyConsts.oidc
function migrateOldConfigs(strategy) {
let oldConfigs = {
'client': {
'clientid': 'client_id',
'clientsecret': 'client_secret',
'callbackurl': 'redirect_uri'
},
'issuer': {
'authorizationurl': 'authorization_endpoint',
'tokenurl': 'token_endpoint',
'userinfourl': 'userinfo_endpoint'
},
'custom': {
'tenantid': 'tenant_id',
'customerid': 'customer_id'
}
}
for (var type in oldConfigs) {
for (const [key, value] of Object.entries(oldConfigs[type])) {
if (Object.hasOwn(strategy, key)) {
if (strategy[type][value] && obj.common.validateString(strategy[type][value])) {
let error = new Error('OIDC: OLD CONFIG: Config conflict, new config overrides old config');
parent.authLog('migrateOldConfigs', `${JSON.stringify(error)} OLD CONFIG: ${key}: ${strategy[key]} NEW CONFIG: ${value}:${strategy[type][value]}`);
} else {
parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
strategy[type][value] = strategy[key];
}
delete strategy[key]
}
}
}
if (typeof strategy.scope == 'string') {
if (!strategy.custom.scope) {
strategy.custom.scope = strategy.scope;
strategy.options.params = { 'scope': strategy.scope };
parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.scope => strategy.custom.scope`);
} else {
let error = new Error('OIDC: OLD CONFIG: Config conflict, using new config values.');
parent.authLog('migrateOldConfigs', `${error.message} OLD CONFIG: strategy.scope: ${strategy.scope} NEW CONFIG: strategy.custom.scope:${strategy.custom.scope}`);
parent.debug('warning', error.message)
}
delete strategy.scope
}
return strategy
}
// Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
function oidcCallback(tokenset, profile, verified) {
// Initialize user object
let user = { 'strategy': 'oidc' }
let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null
user.sid = obj.common.validateString(profile.sub) ? '~oidc:' + profile.sub : null
user.name = obj.common.validateString(profile.name) ? profile.name : null
user.email = obj.common.validateString(profile.email) ? profile.email : null
if (claims != null) {
user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid
user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name
user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email
}
user.emailVerified = profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email),
user.groups = obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null
user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
if (obj.common.validateString(strategy.groups.claim)) {
user.groups = obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
}
// Setup end session enpoint if not already configured this requires an auth token
try {
if (!strategy.issuer.end_session_endpoint) {
strategy.issuer.end_session_endpoint = strategy.obj.client.endSessionUrl({ 'id_token_hint': tokenset })
parent.authLog('oidcCallback', `OIDC: Discovered end_session_endpoint: ${strategy.issuer.end_session_endpoint}`);
}
} catch (err) {
let error = new Error('OIDC: Discovering end_session_endpoint failed. Using Default.', { cause: err });
strategy.issuer.end_session_endpoint = strategy.issuer.issuer + '/logout';
parent.debug('error', `${error.message} end_session_endpoint: ${strategy.issuer.end_session_endpoint} post_logout_redirect_uri: ${strategy.client.post_logout_redirect_uri} TOKENSET: ${JSON.stringify(tokenset)}`);
parent.authLog('oidcCallback', error.message);
}
// Setup presets and groups, get groups from API if needed then return
if (strategy.groups && typeof user.preset == 'string') {
getGroups(user.preset, tokenset).then((groups) => {
user = Object.assign(user, { 'groups': groups });
return verified(null, user);
}).catch((err) => {
let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
parent.debug('error', `${JSON.stringify(error)}`);
parent.authLog('oidcCallback', error.message);
user.groups = [];
return verified(null, user);
});
} else {
return verified(null, user);
}
async function getGroups(preset, tokenset) {
let url = '';
if (preset == 'azure') { url = strategy.groups.recursive == true ? 'https://graph.microsoft.com/v1.0/me/transitiveMemberOf' : 'https://graph.microsoft.com/v1.0/me/memberOf'; }
if (preset == 'google') { url = strategy.custom.customer_id ? 'https://cloudidentity.googleapis.com/v1/groups?parent=customers/' + strategy.custom.customer_id : strategy.custom.identitysource ? 'https://cloudidentity.googleapis.com/v1/groups?parent=identitysources/' + strategy.custom.identitysource : null; }
return new Promise((resolve, reject) => {
const options = {
'headers': { authorization: 'Bearer ' + tokenset.access_token }
}
const req = require('https').get(url, options, (res) => {
let data = []
res.on('data', (chunk) => {
data.push(chunk);
});
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
let error = new Error('OIDC: GROUPS: Bad response code from API, statusCode: ' + res.statusCode);
parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
console.error(error);
reject(error);
}
if (data.length == 0) {
let error = new Error('OIDC: GROUPS: Getting groups from API failed, request returned no data in response.');
parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
console.error(error);
reject(error);
}
try {
if (Buffer.isBuffer(data[0])) {
data = Buffer.concat(data);
data = data.toString();
} else { // else if (typeof data[0] == 'string')
data = data.join();
}
} catch (err) {
let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: err });
parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
console.error(error);
reject(error);
}
if (preset == 'azure') {
data = JSON.parse(data);
if (data.error) {
let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: data.error });
parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
console.error(error);
reject(error);
}
data = data.value;
}
if (preset == 'google') {
data = data.split('\n');
data = data.join('');
data = JSON.parse(data);
data = data.groups;
}
let groups = []
for (var i in data) {
if (typeof data[i].displayName == 'string') {
groups.push(data[i].displayName);
}
}
if (groups.length == 0) {
let warn = new Error('OIDC: GROUPS: No groups returned from API.');
parent.authLog('getGroups', `WARN: ${warn.message} DATA: ${data}`);
console.warn(warn);
resolve(groups);
} else {
resolve(groups);
}
});
});
req.on('error', (err) => {
let error = new Error('OIDC: GROUPS: Request error.', { cause: err });
parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
console.error(error);
reject(error);
});
req.end();
});
}
}
}
return authStrategyFlags;
}
// Handle an incoming request as a web relay
function handleWebRelayRequest(req, res) {
var webRelaySessionId = null;
if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
else if (req.session.z != null) { webRelaySessionId = req.session.z; }
if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
if (relaySession != null) {
// The web relay session is valid, use it
relaySession.handleRequest(req, res);
} else {
// No web relay session with this relay identifier, close the HTTP request.
res.sendStatus(404);
}
} else {
// The user is not logged in or does not have a relay identifier, close the HTTP request.
res.sendStatus(404);
}
}
// Handle an incoming websocket connection as a web relay
function handleWebRelayWebSocket(ws, req) {
var webRelaySessionId = null;
if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
else if (req.session.z != null) { webRelaySessionId = req.session.z; }
if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
if (relaySession != null) {
// The multi-tunnel session is valid, use it
relaySession.handleWebSocket(ws, req);
} else {
// No multi-tunnel session with this relay identifier, close the websocket.
ws.close();
}
} else {
// The user is not logged in or does not have a relay identifier, close the websocket.
ws.close();
}
}
// Perform server inner authentication
// This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
// sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username
// and password or authentication cookie.
function PerformWSSessionInnerAuth(ws, req, domain, func) {
// When data is received from the web socket
ws.on('message', function (data) {
var command;
try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; }
if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
switch (command.action) {
case 'serverAuth': { // This command is used to perform server "inner" authentication.
// Check the client nonce and TLS hash
if ((obj.common.validateString(command.cnonce, 1, 256) == false) || (obj.common.validateString(command.tlshash, 1, 512) == false)) {
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badargs' })); } catch (ex) { }
try { ws.close(); } catch (ex) { }
break;
}
// Check that the TLS hash is an acceptable one.
var h = Buffer.from(command.tlshash, 'hex').toString('binary');
if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) {
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badtlscert' })); } catch (ex) { }
try { ws.close(); } catch (ex) { }
return;
}
// TLS hash check is a success, sign the request.
// Perform the hash signature using the server agent certificate
var nonce = obj.crypto.randomBytes(48);
var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
// Send back our certificate + nonce + signature
ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(obj.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature, 'binary').toString('base64') }));
});
break;
}
case 'userAuth': { // This command is used to perform user authentication.
// Check username and password authentication
if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) {
if ((err != null) || (userid == null)) {
// Invalid authentication
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
try { ws.close(); } catch (ex) { }
} else {
var user = obj.users[userid];
if ((err == null) && (user)) {
// Check if a 2nd factor is needed
const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
// See if we support two-factor trusted cookies
var twoFactorCookieDays = 30;
if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
// Check if two factor can be skipped
const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
if ((twoFactorSkip == null) && (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true)) {
// Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
//var push2fa = ((parent.firebase != null) && (user.otpdev != null));
if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) {
if ((command.token == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA email to: ' + user.email);
domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
// Ask for a login token & confirm email was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((command.token == '**sms**') && (sms2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((command.token == '**msg**') && (msg2fa == true)) {
// Cause a token to be sent to the user's messenger account
user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA message to: ' + user.phone);
parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
/*
} else if ((command.token == '**push**') && (push2fa == true)) {
// Cause push notification to device
const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
});
*/
} else {
// Ask for a login token
parent.debug('web', 'Asking for login token');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (ex) { console.log(ex); }
}
} else {
checkUserOneTimePassword(req, domain, user, command.token, null, function (result, authData) {
if (result == false) {
// Failed, ask for a login token again
parent.debug('web', 'Invalid login token, asking again');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else {
// We are authenticated with 2nd factor.
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else {
// We are authenticated
ws._socket.pause();
ws.removeAllListeners(['message', 'close', 'error']);
func(ws, req, domain, user, authData);
}
}
});
}
} else {
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else {
// We are authenticated
ws._socket.pause();
ws.removeAllListeners(['message', 'close', 'error']);
func(ws, req, domain, user, twoFactorSkip);
}
}
}
}
});
} else {
// Invalid authentication
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
try { ws.close(); } catch (ex) { }
}
break;
}
}
});
// If error, do nothing
ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
// If the web socket is closed
ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
// Resume the socket to perform inner authentication
try { ws._socket.resume(); } catch (ex) { }
}
// Authenticates a session and forwards
function PerformWSSessionAuth(ws, req, noAuthOk, func) {
// Check if the session expired
if ((req.session != null) && (typeof req.session.expire == 'number') && (req.session.expire <= Date.now())) {
parent.debug('web', 'WSERROR: Session expired.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'expired', msg: 'expired-1' })); ws.close(); } catch (e) { } return;
}
// Check if this is a banned ip address
if (obj.checkAllowLogin(req) == false) { parent.debug('web', 'WSERROR: Banned connection.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'banned', msg: 'banned-1' })); ws.close(); } catch (e) { } return; }
try {
// Hold this websocket until we are ready.
ws._socket.pause();
// Check IP filtering and domain
var domain = null;
if (noAuthOk == true) {
domain = getDomain(req);
if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, no auth ok.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-1' })); ws.close(); return; } catch (e) { } return; }
} else {
// If authentication is required, enforce IP address filtering.
domain = checkUserIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, user auth required.'); return; }
}
// Check if inner authentication is requested
if (req.headers['x-meshauth'] === '*') { func(ws, req, domain, null); return; }
const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
// A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
if ((req.query.user != null) && (req.query.pass != null)) {
// A user/pass is provided in URL arguments
obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
var user = obj.users[userid];
// Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
if ((user != null) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
// No tools allowed, close the websocket connection
parent.debug('web', 'ERR: Websocket no tools allowed');
try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
return;
}
// See if we support two-factor trusted cookies
var twoFactorCookieDays = 30;
if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
if ((err == null) && (user)) {
// Check if a 2nd factor is needed
if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
// Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
//var push2fa = ((parent.firebase != null) && (user.otpdev != null));
if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) {
if ((req.query.token == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA email to: ' + user.email);
domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
// Ask for a login token & confirm email was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((req.query.token == '**sms**') && (sms2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((req.query.token == '**msg**') && (msg2fa == true)) {
// Cause a token to be sent to the user's messenger account
user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm message was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
/*
} else if ((command.token == '**push**') && (push2fa == true)) {
// Cause push notification to device
const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
});
*/
} else {
// Ask for a login token
parent.debug('web', 'Asking for login token');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
}
} else {
checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result, authData) {
if (result == false) {
// Failed, ask for a login token again
parent.debug('web', 'Invalid login token, asking again');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else {
// We are authenticated with 2nd factor.
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else {
func(ws, req, domain, user, null, authData);
}
}
});
}
} else {
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else {
// We are authenticated
func(ws, req, domain, user);
}
}
} else {
// Failed to authenticate, see if a default user is active
if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
// A default user is active
func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
} else {
// If not authenticated, close the websocket connection
parent.debug('web', 'ERR: Websocket bad user/pass auth');
//obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp });
//obj.setbadLogin(req);
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2a' })); ws.close(); } catch (e) { }
}
}
});
return;
}
if ((req.query.auth != null) && (req.query.auth != '')) {
// This is a encrypted cookie authentication
var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
if ((cookie != null) && (cookie.ip != null) && !checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
cookie = null;
}
if ((cookie != null) && (cookie.userid != null) && (obj.users[cookie.userid]) && (cookie.domainid == domain.id) && (cookie.userid.split('/')[1] == domain.id)) {
// Valid cookie, we are authenticated. Cookie of format { userid: 'user//name', domain: '' }
func(ws, req, domain, obj.users[cookie.userid], cookie);
return;
} else if ((cookie != null) && (cookie.a === 3) && (typeof cookie.u == 'string') && (obj.users[cookie.u]) && (cookie.u.split('/')[1] == domain.id)) {
// Valid cookie, we are authenticated. Cookie of format { u: 'user//name', a: 3 }
func(ws, req, domain, obj.users[cookie.u], cookie);
return;
} else if ((cookie != null) && (cookie.nouser === 1)) {
// This is a valid cookie, but no user. This is used for agent self-sharing.
func(ws, req, domain, null, cookie);
return;
} /*else {
// This is a bad cookie, keep going anyway, maybe we have a active session that will save us.
if ((cookie != null) && (cookie.domainid != domain.id)) { parent.debug('web', 'ERR: Invalid domain, got \"' + cookie.domainid + '\", expected \"' + domain.id + '\".'); }
parent.debug('web', 'ERR: Websocket bad cookie auth (Cookie:' + (cookie != null) + '): ' + req.query.auth);
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2b' })); ws.close(); } catch (e) { }
return;
}
*/
}
if (req.headers['x-meshauth'] != null) {
// This is authentication using a custom HTTP header
var s = req.headers['x-meshauth'].split(',');
for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); }
if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; }
obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) {
var user = obj.users[userid];
if ((err == null) && (user)) {
// Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
if ((user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
// No tools allowed, close the websocket connection
parent.debug('web', 'ERR: Websocket no tools allowed');
try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
return;
}
// Check if a 2nd factor is needed
if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
// See if we support two-factor trusted cookies
var twoFactorCookieDays = 30;
if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
// Figure out if email 2FA is allowed
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
if (s.length != 3) {
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else {
checkUserOneTimePassword(req, domain, user, s[2], null, function (result, authData) {
if (result == false) {
if ((s[2] == '**email**') && (email2fa == true)) {
// Cause a token to be sent to the user's registered email
user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA email to: ' + user.email);
domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
// Ask for a login token & confirm email was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((s[2] == '**sms**') && (sms2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else if ((s[2] == '**msg**') && (msg2fa == true)) {
// Cause a token to be sent to the user's phone number
user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
obj.db.SetUser(user);
parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
// Ask for a login token & confirm sms was sent
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else {
// Ask for a login token
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
}
} else {
// We are authenticated with 2nd factor.
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
} else {
func(ws, req, domain, user, null, authData);
}
}
});
}
} else {
// We are authenticated
// Check email verification
if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
parent.debug('web', 'Invalid login, asking for email validation');
try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
} else {
func(ws, req, domain, user);
}
}
} else {
// Failed to authenticate, see if a default user is active
if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
// A default user is active
func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
} else {
// If not authenticated, close the websocket connection
parent.debug('web', 'ERR: Websocket bad user/pass auth');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2d' })); ws.close(); } catch (e) { }
}
}
});
return;
}
if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
// A default user is active
func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
return;
}
if (req.session && (req.session.userid != null) && (req.session.userid.split('/')[1] == domain.id) && (obj.users[req.session.userid])) {
// This user is logged in using the ExpressJS session
func(ws, req, domain, obj.users[req.session.userid]);
return;
}
if (noAuthOk != true) {
// If not authenticated, close the websocket connection
parent.debug('web', 'ERR: Websocket no auth');
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-4' })); ws.close(); } catch (e) { }
} else {
// Continue this session without user authentication,
// this is expected if the agent is connecting for a tunnel.
func(ws, req, domain, null);
}
} catch (e) { console.log(e); }
}
// Find a free port starting with the specified one and going up.
function CheckListenPort(port, addr, func) {
var s = obj.net.createServer(function (socket) { });
obj.tcpServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on('error', function (err) {
if (args.exactports) { console.error('ERROR: MeshCentral HTTPS server port ' + port + ' not available.'); process.exit(); }
else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } }
});
}
// Start the ExpressJS web server
function StartWebServer(port, addr) {
if ((port < 1) || (port > 65535)) return;
obj.args.port = port;
if (obj.tlsServer != null) {
if (obj.args.lanonly == true) {
obj.tcpServer = obj.tlsServer.listen(port, addr, function () { console.log('MeshCentral HTTPS server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); });
} else {
obj.tcpServer = obj.tlsServer.listen(port, addr, function () {
console.log('MeshCentral HTTPS server running on ' + certificates.CommonName + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
if (args.relaydns != null) { console.log('MeshCentral HTTPS relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
});
obj.parent.updateServerState('servername', certificates.CommonName);
}
obj.parent.debug('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
obj.parent.updateServerState('https-port', port);
if (args.aliasport != null) { obj.parent.updateServerState('https-aliasport', args.aliasport); }
} else {
obj.tcpServer = obj.app.listen(port, addr, function () {
console.log('MeshCentral HTTP server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
if (args.relaydns != null) { console.log('MeshCentral HTTP relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
});
obj.parent.updateServerState('http-port', port);
if (args.aliasport != null) { obj.parent.updateServerState('http-aliasport', args.aliasport); }
}
// Check if there is a permissions problem with the ports.
if (require('os').platform() != 'win32') {
var expectedPort = obj.parent.config.settings.port ? obj.parent.config.settings.port : 443;
if ((expectedPort != port) && (port >= 1024) && (port < 1034)) {
console.log('');
console.log('WARNING: MeshCentral is running without permissions to use ports below 1025.');
console.log(' Use setcap to grant access to lower ports, or read installation guide.');
console.log('');
console.log(' sudo setcap \'cap_net_bind_service=+ep\' `which node` \r\n');
obj.parent.addServerWarning('Server running without permissions to use ports below 1025.', false);
}
}
}
// Start the ExpressJS web server on agent-only alternative port
function StartAltWebServer(port, addr) {
if ((port < 1) || (port > 65535)) return;
var agentAliasPort = null;
var agentAliasDns = null;
if (args.agentaliasport != null) { agentAliasPort = args.agentaliasport; }
if (args.agentaliasdns != null) { agentAliasDns = args.agentaliasdns; }
if (obj.tlsAltServer != null) {
if (obj.args.lanonly == true) {
obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
} else {
obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on ' + ((agentAliasDns != null) ? agentAliasDns : certificates.CommonName) + ':' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
}
obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
obj.parent.updateServerState('https-agent-port', port);
} else {
obj.tcpAltServer = obj.agentapp.listen(port, addr, function () { console.log('MeshCentral HTTP agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
obj.parent.updateServerState('http-agent-port', port);
}
}
// Force mesh agent disconnection
obj.forceMeshAgentDisconnect = function (user, domain, nodeid, disconnectMode) {
if (nodeid == null) return;
var splitnode = nodeid.split('/');
if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
var agent = obj.wsagents[nodeid];
if (agent == null) return;
// Check we have agent rights
if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
};
// Send the core module to the mesh agent
obj.sendMeshAgentCore = function (user, domain, nodeid, coretype, coredata) {
if (nodeid == null) return;
const splitnode = nodeid.split('/');
if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
// TODO: This command only works if the agent is connected on the same server. Will not work with multi server peering.
const agent = obj.wsagents[nodeid];
if (agent == null) return;
// Check we have agent rights
if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) {
if (coretype == 'clear') {
// Clear the mesh agent core
agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0));
} else if (coretype == 'default') {
// Reset to default code
agent.agentCoreCheck = 0; // Tell the agent object we are using a default code
agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
} else if (coretype == 'recovery') {
// Reset to recovery core
agent.agentCoreCheck = 1001; // Tell the agent object we are using the recovery core.
agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
} else if (coretype == 'tiny') {
// Reset to tiny core
agent.agentCoreCheck = 1011; // Tell the agent object we are using the tiny core.
agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
} else if (coretype == 'custom') {
agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
var buf = Buffer.from(coredata, 'utf8');
const hash = obj.crypto.createHash('sha384').update(buf).digest().toString('binary'); // Perform a SHA384 hash on the core module
agent.sendBinary(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + buf.toString('binary')); // Send the code module to the agent
}
}
};
// Get the server path of a user or mesh object
function getServerRootFilePath(obj) {
if ((typeof obj != 'object') || (obj.domain == null) || (obj._id == null)) return null;
var domainname = 'domain', splitname = obj._id.split('/');
if (splitname.length != 3) return null;
if (obj.domain !== '') domainname = 'domain-' + obj.domain;
return obj.path.join(obj.filespath, domainname + "/" + splitname[0] + "-" + splitname[2]);
}
// Return true is the input string looks like an email address
function checkEmail(str) {
var x = str.split('@');
var ok = ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2));
if (ok == true) { var y = x[1].split('.'); for (var i in y) { if (y[i].length == 0) { ok = false; } } }
return ok;
}
/*
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 ]
*/
// Count sessions and event any changes
obj.recountSessions = function (changedSessionId) {
var userid, oldcount, newcount, x, serverid;
if (changedSessionId == null) {
// Recount all sessions
// Calculate the session count for all userid's
var newSessionsCount = {};
for (userid in obj.wssessions) { newSessionsCount[userid] = obj.wssessions[userid].length; }
for (serverid in obj.wsPeerSessions3) {
for (userid in obj.wsPeerSessions3[serverid]) {
x = obj.wsPeerSessions3[serverid][userid].length;
if (newSessionsCount[userid] == null) { newSessionsCount[userid] = x; } else { newSessionsCount[userid] += x; }
}
}
// See what session counts have changed, event any changes
for (userid in newSessionsCount) {
newcount = newSessionsCount[userid];
oldcount = obj.sessionsCount[userid];
if (oldcount == null) { oldcount = 0; } else { delete obj.sessionsCount[userid]; }
if (newcount != oldcount) {
x = userid.split('/');
var u = obj.users[userid];
if (u) {
var targets = ['*', 'server-users'];
if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
}
}
}
// If there are any counts left in the old counts, event to zero
for (userid in obj.sessionsCount) {
oldcount = obj.sessionsCount[userid];
if ((oldcount != null) && (oldcount != 0)) {
x = userid.split('/');
var u = obj.users[userid];
if (u) {
var targets = ['*', 'server-users'];
if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', userid: userid, username: x[2], count: 0, domain: x[1], nolog: 1, nopeers: 1 })
}
}
}
// Set the new session counts
obj.sessionsCount = newSessionsCount;
} else {
// Figure out the userid
userid = changedSessionId.split('/').slice(0, 3).join('/');
// Recount only changedSessionId
newcount = 0;
if (obj.wssessions[userid] != null) { newcount = obj.wssessions[userid].length; }
for (serverid in obj.wsPeerSessions3) { if (obj.wsPeerSessions3[serverid][userid] != null) { newcount += obj.wsPeerSessions3[serverid][userid].length; } }
oldcount = obj.sessionsCount[userid];
if (oldcount == null) { oldcount = 0; }
// If the count changed, update and event
if (newcount != oldcount) {
x = userid.split('/');
var u = obj.users[userid];
if (u) {
var targets = ['*', 'server-users'];
if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
obj.sessionsCount[userid] = newcount;
}
}
}
};
/* Access Control Functions */
// Remove user rights
function removeUserRights(rights, user) {
if (user.removeRights == null) return rights;
var add = 0, substract = 0;
if ((user.removeRights & 0x00000008) != 0) { substract += 0x00000008; } // No Remote Control
if ((user.removeRights & 0x00010000) != 0) { add += 0x00010000; } // No Desktop
if ((user.removeRights & 0x00000100) != 0) { add += 0x00000100; } // Desktop View Only
if ((user.removeRights & 0x00000200) != 0) { add += 0x00000200; } // No Terminal
if ((user.removeRights & 0x00000400) != 0) { add += 0x00000400; } // No Files
if ((user.removeRights & 0x00000010) != 0) { substract += 0x00000010; } // No Console
if ((user.removeRights & 0x00008000) != 0) { substract += 0x00008000; } // No Uninstall
if ((user.removeRights & 0x00020000) != 0) { substract += 0x00020000; } // No Remote Command
if ((user.removeRights & 0x00000040) != 0) { substract += 0x00000040; } // No Wake
if ((user.removeRights & 0x00040000) != 0) { substract += 0x00040000; } // No Reset/Off
if (rights != 0xFFFFFFFF) {
// If not administrator, add and subsctract restrictions
rights |= add;
rights &= (0xFFFFFFFF - substract);
} else {
// If administrator for a device group, start with permissions and add and subsctract restrictions
rights = 1 + 2 + 4 + 8 + 32 + 64 + 128 + 16384 + 32768 + 131072 + 262144 + 524288 + 1048576;
rights |= add;
rights &= (0xFFFFFFFF - substract);
}
return rights;
}
// Return the node and rights for a array of nodeids
obj.GetNodesWithRights = function (domain, user, nodeids, func) {
var rc = nodeids.length, r = {};
for (var i in nodeids) {
obj.GetNodeWithRights(domain, user, nodeids[i], function (node, rights, visible) {
if ((node != null) && (visible == true)) { r[node._id] = { node: node, rights: rights }; if (--rc == 0) { func(r); } }
});
}
}
// Return the node and rights for a given nodeid
obj.GetNodeWithRights = function (domain, user, nodeid, func) {
// Perform user pre-validation
if ((user == null) || (nodeid == null)) { func(null, 0, false); return; } // Invalid user
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { func(null, 0, false); return; } // No rights
// Perform node pre-validation
if (obj.common.validateString(nodeid, 0, 128) == false) { func(null, 0, false); return; } // Invalid nodeid
const snode = nodeid.split('/');
if ((snode.length != 3) || (snode[0] != 'node')) { func(null, 0, false); return; } // Invalid nodeid
if ((domain != null) && (snode[1] != domain.id)) { func(null, 0, false); return; } // Invalid domain
// Check that we have permissions for this node.
db.Get(nodeid, function (err, nodes) {
if ((nodes == null) || (nodes.length != 1)) { func(null, 0, false); return; } // No such nodeid
// This is a super user that can see all device groups for a given domain
if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (nodes[0].domain == user.domain)) {
func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return;
}
// If no links, stop here.
if (user.links == null) { func(null, 0, false); return; }
// Check device link
var rights = 0, visible = false, r = user.links[nodeid];
if (r != null) {
if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device link, stop here.
rights |= r.rights;
visible = true;
}
// Check device group link
r = user.links[nodes[0].meshid];
if (r != null) {
if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device group link, stop here.
rights |= r.rights;
visible = true;
}
// Check user group links
for (var i in user.links) {
if (i.startsWith('ugrp/')) {
const g = obj.userGroups[i];
if (g && (g.links != null)) {
r = g.links[nodes[0].meshid];
if (r != null) {
if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group link, stop here.
rights |= r.rights; // TODO: Deal with reverse rights
visible = true;
}
r = g.links[nodeid];
if (r != null) {
if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group direct link, stop here.
rights |= r.rights; // TODO: Deal with reverse rights
visible = true;
}
}
}
}
// Remove any user rights
rights = removeUserRights(rights, user);
// Return the rights we found
func(nodes[0], rights, visible);
});
}
// Returns a list of all meshes that this user has some rights too
obj.GetAllMeshWithRights = function (user, rights) {
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { return []; }
var r = [];
if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0)) {
// This is a super user that can see all device groups for a given domain
var meshStartStr = 'mesh/' + user.domain + '/';
for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]); } }
return r;
}
if (user.links == null) { return []; }
for (var i in user.links) {
if (i.startsWith('mesh/')) {
// Grant access to a device group thru a direct link
const m = obj.meshes[i];
if ((m) && (r.indexOf(m) == -1) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) { r.push(m); }
} else if (i.startsWith('ugrp/')) {
// Grant access to a device group thru a user group
const g = obj.userGroups[i];
for (var j in g.links) {
if (j.startsWith('mesh/') && ((rights == null) || ((g.links[j].rights != null) && (g.links[j].rights & rights) != 0))) {
const m = obj.meshes[j];
if ((m) && (m.deleted == null) && (r.indexOf(m) == -1)) { r.push(m); }
}
}
}
}
return r;
}
// Returns a list of all mesh id's that this user has some rights too
obj.GetAllMeshIdWithRights = function (user, rights) {
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { return []; }
var r = [];
if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0)) {
// This is a super user that can see all device groups for a given domain
var meshStartStr = 'mesh/' + user.domain + '/';
for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]._id); } }
return r;
}
if (user.links == null) { return []; }
for (var i in user.links) {
if (i.startsWith('mesh/')) {
// Grant access to a device group thru a direct link
const m = obj.meshes[i];
if ((m) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
if (r.indexOf(m._id) == -1) { r.push(m._id); }
}
} else if (i.startsWith('ugrp/')) {
// Grant access to a device group thru a user group
const g = obj.userGroups[i];
if (g && (g.links != null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
for (var j in g.links) {
if (j.startsWith('mesh/')) {
const m = obj.meshes[j];
if ((m) && (m.deleted == null)) {
if (r.indexOf(m._id) == -1) { r.push(m._id); }
}
}
}
}
}
}
return r;
}
// Get the rights of a user on a given device group
obj.GetMeshRights = function (user, mesh) {
if ((user == null) || (mesh == null)) { return 0; }
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { return 0; }
var r, meshid;
if (typeof mesh == 'string') {
meshid = mesh;
} else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
meshid = mesh._id;
} else return 0;
// Check if this is a super user that can see all device groups for a given domain
if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return removeUserRights(0xFFFFFFFF, user); }
// Check direct user to device group permissions
if (user.links == null) return 0;
var rights = 0;
r = user.links[meshid];
if (r != null) {
var rights = r.rights;
if (rights == 0xFFFFFFFF) { return removeUserRights(rights, user); } // If the user has full access thru direct link, stop here.
}
// Check if we are part of any user groups that would give this user more access.
for (var i in user.links) {
if (i.startsWith('ugrp')) {
const g = obj.userGroups[i];
if (g) {
r = g.links[meshid];
if (r != null) {
if (r.rights == 0xFFFFFFFF) {
return removeUserRights(r.rights, user); // If the user hash full access thru a user group link, stop here.
} else {
rights |= r.rights; // Add to existing rights (TODO: Deal with reverse rights)
}
}
}
}
}
return removeUserRights(rights, user);
}
// Returns true if the user can view the given device group
obj.IsMeshViewable = function (user, mesh) {
if ((user == null) || (mesh == null)) { return false; }
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { return false; }
var meshid;
if (typeof mesh == 'string') {
meshid = mesh;
} else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
meshid = mesh._id;
} else return false;
// Check if this is a super user that can see all device groups for a given domain
if ((user.siteadmin == 0xFFFFFFFF) && (parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return true; }
// Check direct user to device group permissions
if (user.links == null) { return false; }
if (user.links[meshid] != null) { return true; } // If the user has a direct link, stop here.
// Check if we are part of any user groups that would give this user visibility to this device group.
for (var i in user.links) {
if (i.startsWith('ugrp')) {
const g = obj.userGroups[i];
if (g && (g.links[meshid] != null)) { return true; } // If the user has a user group link, stop here.
}
}
return false;
}
var GetNodeRightsCache = {};
var GetNodeRightsCacheCount = 0;
// Return the user rights for a given node
obj.GetNodeRights = function (user, mesh, nodeid) {
if ((user == null) || (mesh == null) || (nodeid == null)) { return 0; }
if (typeof user == 'string') { user = obj.users[user]; }
if (user == null) { return 0; }
var meshid;
if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; } else return 0;
// Check if we have this in the cache
const cacheid = user._id + '/' + meshid + '/' + nodeid;
const cache = GetNodeRightsCache[cacheid];
if (cache != null) { if (cache.t > Date.now()) { return cache.o; } else { GetNodeRightsCacheCount--; } } // Cache hit, or we need to update the cache
if (GetNodeRightsCacheCount > 2000) { GetNodeRightsCache = {}; GetNodeRightsCacheCount = 0; } // From time to time, flush the cache
var r = obj.GetMeshRights(user, mesh);
if (r == 0xFFFFFFFF) {
const out = removeUserRights(r, user);
GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
GetNodeRightsCacheCount++;
return out;
}
// Check direct device rights using device data
if ((user.links != null) && (user.links[nodeid] != null)) { r |= user.links[nodeid].rights; } // TODO: Deal with reverse permissions
if (r == 0xFFFFFFFF) {
const out = removeUserRights(r, user);
GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
GetNodeRightsCacheCount++;
return out;
}
// Check direct device rights thru a user group
for (var i in user.links) {
if (i.startsWith('ugrp')) {
const g = obj.userGroups[i];
if (g && (g.links[nodeid] != null)) { r |= g.links[nodeid].rights; }
}
}
const out = removeUserRights(r, user);
GetNodeRightsCache[cacheid] = { t: Date.now() + 10000, o: out };
GetNodeRightsCacheCount++;
return out;
}
// Returns a list of displatch targets for a given mesh
// We have to target the meshid and all user groups for this mesh, plus any added targets
obj.CreateMeshDispatchTargets = function (mesh, addedTargets) {
var targets = (addedTargets != null) ? addedTargets : [];
if (targets.indexOf('*') == -1) { targets.push('*'); }
if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
return targets;
}
// Returns a list of displatch targets for a given mesh
// We have to target the meshid and all user groups for this mesh, plus any added targets
obj.CreateNodeDispatchTargets = function (mesh, nodeid, addedTargets) {
var targets = (addedTargets != null) ? addedTargets : [];
targets.push(nodeid);
if (targets.indexOf('*') == -1) { targets.push('*'); }
if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
for (var i in obj.userGroups) { const g = obj.userGroups[i]; if ((g != null) && (g.links != null) && (g.links[nodeid] != null)) { targets.push(i); } }
return targets;
}
// Clone a safe version of a user object, remove everything that is secret.
obj.CloneSafeUser = function (user) {
if (typeof user != 'object') { return user; }
var user2 = Object.assign({}, user); // Shallow clone
delete user2.hash;
delete user2.passhint;
delete user2.salt;
delete user2.type;
delete user2.domain;
delete user2.subscriptions;
delete user2.passtype;
delete user2.otpsms;
delete user2.otpmsg;
if ((typeof user2.otpekey == 'object') && (user2.otpekey != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
if ((typeof user2.otpdev == 'string') && (user2.otpdev != null)) { user2.otpdev = 1; } // Indicates device for 2FA push notification
if ((typeof user2.webpush == 'object') && (user2.webpush != null)) { user2.webpush = user2.webpush.length; } // Indicates the number of web push sessions we have
return user2;
}
// Clone a safe version of a node object, remove everything that is secret.
obj.CloneSafeNode = function (node) {
if (typeof node != 'object') { return node; }
var r = node;
if ((r.pmt != null) || (r.ssh != null) || (r.rdp != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
r = Object.assign({}, r); // Shallow clone
if (r.pmt != null) { r.pmt = 1; }
if (r.ssh != null) {
var n = {};
for (var i in r.ssh) {
if (i.startsWith('user/')) {
if (r.ssh[i].p) { n[i] = 1; } // Username and password
else if (r.ssh[i].k && r.ssh[i].kp) { n[i] = 2; } // Username, key and password
else if (r.ssh[i].k) { n[i] = 3; } // Username and key. No password.
}
}
r.ssh = n;
}
if (r.rdp != null) { var n = {}; for (var i in r.rdp) { if (i.startsWith('user/')) { n[i] = 1; } } r.rdp = n; }
if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
r.intelamt = Object.assign({}, r.intelamt); // Shallow clone
if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node
if (r.intelamt.mpspass != null) { r.intelamt.mpspass = 1; }; // Remove the Intel AMT MPS password from the node
}
}
return r;
}
// Clone a safe version of a mesh object, remove everything that is secret.
obj.CloneSafeMesh = function (mesh) {
if (typeof mesh != 'object') { return mesh; }
var r = mesh;
if (((r.amt != null) && (r.amt.password != null)) || ((r.kvm != null) && (r.kvm.pass != null))) {
r = Object.assign({}, r); // Shallow clone
if ((r.amt != null) && (r.amt.password != null)) {
r.amt = Object.assign({}, r.amt); // Shallow clone
if ((r.amt.password != null) && (r.amt.password != '')) { r.amt.password = 1; } // Remove the Intel AMT password from the policy
}
if ((r.kvm != null) && (r.kvm.pass != null)) {
r.kvm = Object.assign({}, r.kvm); // Shallow clone
if ((r.kvm.pass != null) && (r.kvm.pass != '')) { r.kvm.pass = 1; } // Remove the IP KVM device password
}
}
return r;
}
// Filter the user web site and only output state that we need to keep
const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar'];
const acceptableUserWebStateDesktopStrings = ['encoding', 'showfocus', 'showmouse', 'showcad', 'limitFrameRate', 'noMouseRotate', 'quality', 'scaling']
obj.filterUserWebState = function (state) {
if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }
if ((state == null) || (typeof state != 'object')) { return null; }
var out = {};
for (var i in acceptableUserWebStateStrings) {
var n = acceptableUserWebStateStrings[i];
if ((state[n] != null) && ((typeof state[n] == 'number') || (typeof state[n] == 'boolean') || ((typeof state[n] == 'string') && (state[n].length < 64)))) { out[n] = state[n]; }
}
if ((typeof state.stars == 'string') && (state.stars.length < 2048)) { out.stars = state.stars; }
if (typeof state.desktopsettings == 'string') { try { state.desktopsettings = JSON.parse(state.desktopsettings); } catch (ex) { delete state.desktopsettings; } }
if (state.desktopsettings != null) {
out.desktopsettings = {};
for (var i in acceptableUserWebStateDesktopStrings) {
var n = acceptableUserWebStateDesktopStrings[i];
if ((state.desktopsettings[n] != null) && ((typeof state.desktopsettings[n] == 'number') || (typeof state.desktopsettings[n] == 'boolean') || ((typeof state.desktopsettings[n] == 'string') && (state.desktopsettings[n].length < 32)))) { out.desktopsettings[n] = state.desktopsettings[n]; }
}
out.desktopsettings = JSON.stringify(out.desktopsettings);
}
if ((typeof state.deskKeyShortcuts == 'string') && (state.deskKeyShortcuts.length < 2048)) { out.deskKeyShortcuts = state.deskKeyShortcuts; }
if ((typeof state.deskStrings == 'string') && (state.deskStrings.length < 10000)) { out.deskStrings = state.deskStrings; }
if ((typeof state.runopt == 'string') && (state.runopt.length < 30000)) { out.runopt = state.runopt; }
return JSON.stringify(out);
}
// Return the correct render page given mobile, minify and override path.
function getRenderPage(pagename, req, domain) {
var mobile = isMobileBrowser(req), minify = (domain.minify == true), p;
if (req.query.mobile == '1') { mobile = true; } else if (req.query.mobile == '0') { mobile = false; }
if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
if ((domain != null) && (domain.mobilesite === false)) { mobile = false; }
if (mobile) {
if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
if (minify) {
p = obj.path.join(domain.webviewspath, pagename + '-mobile-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
}
p = obj.path.join(domain.webviewspath, pagename + '-mobile');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
}
if (obj.parent.webViewsOverridePath != null) {
if (minify) {
p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
}
p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
}
if (minify) {
p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify document
}
p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile document
}
if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
if (minify) {
p = obj.path.join(domain.webviewspath, pagename + '-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
}
p = obj.path.join(domain.webviewspath, pagename);
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
}
if (obj.parent.webViewsOverridePath != null) {
if (minify) {
p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
}
p = obj.path.join(obj.parent.webViewsOverridePath, pagename);
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
}
if (minify) {
p = obj.path.join(obj.parent.webViewsPath, pagename + '-min');
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify document
}
p = obj.path.join(obj.parent.webViewsPath, pagename);
if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Default document
return null;
}
// Return the correct render page arguments.
function getRenderArgs(xargs, req, domain, page) {
var minify = (domain.minify == true);
if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
xargs.min = minify ? '-min' : '';
xargs.titlehtml = domain.titlehtml;
xargs.title = (domain.title != null) ? domain.title : 'MeshCentral';
if (
((page == 'login2') && (domain.loginpicture == null) && (domain.titlehtml == null)) ||
((page != 'login2') && (domain.titlepicture == null) && (domain.titlehtml == null))
) {
if (domain.title == null) {
xargs.title1 = 'MeshCentral';
xargs.title2 = '';
} else {
xargs.title1 = domain.title;
xargs.title2 = domain.title2 ? domain.title2 : '';
}
} else {
xargs.title1 = domain.title1 ? domain.title1 : '';
xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
}
xargs.extitle = encodeURIComponent(xargs.title).split('\'').join('\\\'');
xargs.domainurl = domain.url;
xargs.autocomplete = (domain.autocomplete === false) ? 'x' : 'autocomplete'; // This option allows autocomplete to be turned off on the login page.
if (typeof domain.hide == 'number') { xargs.hide = domain.hide; }
// To mitigate any possible BREACH attack, we generate a random 0 to 255 bytes length string here.
xargs.randomlength = (args.webpagelengthrandomization !== false) ? parent.crypto.randomBytes(parent.crypto.randomBytes(1)[0]).toString('base64') : '';
return xargs;
}
// Route a command from a agent. domainid, nodeid and meshid are the values of the source agent.
obj.routeAgentCommand = function (command, domainid, nodeid, meshid) {
// Route a message.
// If this command has a sessionid, that is the target.
if (command.sessionid != null) {
if (typeof command.sessionid != 'string') return;
var splitsessionid = command.sessionid.split('/');
// Check that we are in the same domain and the user has rights over this node.
if ((splitsessionid.length == 4) && (splitsessionid[0] == 'user') && (splitsessionid[1] == domainid)) {
// Check if this user has rights to get this message
if (obj.GetNodeRights(splitsessionid[0] + '/' + splitsessionid[1] + '/' + splitsessionid[2], meshid, nodeid) == 0) return; // TODO: Check if this is ok
// See if the session is connected. If so, go ahead and send this message to the target node
var ws = obj.wssessions2[command.sessionid];
if (ws != null) {
command.nodeid = nodeid; // Set the nodeid, required for responses.
delete command.sessionid; // Remove the sessionid, since we are sending to that sessionid, so it's implyed.
try { ws.send(JSON.stringify(command)); } catch (ex) { }
} else if (parent.multiServer != null) {
// See if we can send this to a peer server
var serverid = obj.wsPeerSessions2[command.sessionid];
if (serverid != null) {
command.fromNodeid = nodeid;
parent.multiServer.DispatchMessageSingleServer(command, serverid);
}
}
}
} else if (command.userid != null) { // If this command has a userid, that is the target.
if (typeof command.userid != 'string') return;
var splituserid = command.userid.split('/');
// Check that we are in the same domain and the user has rights over this node.
if ((splituserid[0] == 'user') && (splituserid[1] == domainid)) {
// Check if this user has rights to get this message
if (obj.GetNodeRights(command.userid, meshid, nodeid) == 0) return; // TODO: Check if this is ok
// See if the session is connected
var sessions = obj.wssessions[command.userid];
// Go ahead and send this message to the target node
if (sessions != null) {
command.nodeid = nodeid; // Set the nodeid, required for responses.
delete command.userid; // Remove the userid, since we are sending to that userid, so it's implyed.
for (i in sessions) { sessions[i].send(JSON.stringify(command)); }
}
if (parent.multiServer != null) {
// TODO: Add multi-server support
}
}
} else { // Route this command to all users with MESHRIGHT_AGENTCONSOLE rights to this device group
command.nodeid = nodeid;
var cmdstr = JSON.stringify(command);
// Find all connected user sessions with access to this device
for (var userid in obj.wssessions) {
var xsessions = obj.wssessions[userid];
if (obj.GetNodeRights(userid, meshid, nodeid) != 0) {
// Send the message to all sessions for this user on this server
for (i in xsessions) { try { xsessions[i].send(cmdstr); } catch (e) { } }
}
}
// Send the message to all users of other servers
if (parent.multiServer != null) {
delete command.nodeid;
command.fromNodeid = nodeid;
command.meshid = meshid;
parent.multiServer.DispatchMessage(command);
}
}
}
// Returns a list of acceptable languages in order
obj.getLanguageCodes = function (req) {
// If a user set a localization, use that
if ((req.query.lang == null) && (req.session != null) && (req.session.userid)) {
var user = obj.users[req.session.userid];
if ((user != null) && (user.lang != null)) { req.query.lang = user.lang; }
};
// Get a list of acceptable languages in order
var acceptLanguages = [];
if (req.query.lang != null) {
acceptLanguages.push(req.query.lang.toLowerCase());
} else {
if (req.headers['accept-language'] != null) {
var acceptLanguageSplit = req.headers['accept-language'].split(';');
for (var i in acceptLanguageSplit) {
var acceptLanguageSplitEx = acceptLanguageSplit[i].split(',');
for (var j in acceptLanguageSplitEx) { if (acceptLanguageSplitEx[j].startsWith('q=') == false) { acceptLanguages.push(acceptLanguageSplitEx[j].toLowerCase()); } }
}
}
}
return acceptLanguages;
}
// Render a page using the proper language
function render(req, res, filename, args, user) {
if (obj.renderPages != null) {
// Get the list of acceptable languages in order
var acceptLanguages = obj.getLanguageCodes(req);
// Take a look at the options we have for this file
var fileOptions = obj.renderPages[obj.path.basename(filename)];
if (fileOptions != null) {
for (var i in acceptLanguages) {
if ((acceptLanguages[i] == 'en') || (acceptLanguages[i].startsWith('en-'))) {
// English requested
args.lang = 'en';
if (user && user.llang) { delete user.llang; obj.db.SetUser(user); } // Clear user 'last language' used if needed. Since English is the default, remove "last language".
break;
}
// See if a language (like "fr-ca") or short-language (like "fr") matches an available translation file.
var foundLanguage = null;
if (fileOptions[acceptLanguages[i]] != null) { foundLanguage = acceptLanguages[i]; } else {
const ptr = acceptLanguages[i].indexOf('-');
if (ptr >= 0) {
const shortAcceptedLanguage = acceptLanguages[i].substring(0, ptr);
if (fileOptions[shortAcceptedLanguage] != null) { foundLanguage = shortAcceptedLanguage; }
}
}
// If a language is found, render it.
if (foundLanguage != null) {
// Found a match. If the file no longer exists, default to English.
obj.fs.exists(fileOptions[foundLanguage] + '.handlebars', function (exists) {
if (exists) { args.lang = foundLanguage; res.render(fileOptions[foundLanguage], args); } else { args.lang = 'en'; res.render(filename, args); }
});
if (user && (user.llang != foundLanguage)) { user.llang = foundLanguage; obj.db.SetUser(user); } // Set user 'last language' used if needed.
return;
}
}
}
}
// No matches found, render the default English page.
res.render(filename, args);
}
// Get the list of pages with different languages that can be rendered
function getRenderList() {
// Fetch default rendeing pages
var translateFolder = null;
if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
if (translateFolder != null) {
obj.renderPages = {};
obj.renderLanguages = ['en'];
var files = obj.fs.readdirSync(translateFolder);
for (var i in files) {
var name = files[i];
if (name.endsWith('.handlebars')) {
name = name.substring(0, name.length - 11);
var xname = name.split('_');
if (xname.length == 2) {
if (obj.renderPages[xname[0]] == null) { obj.renderPages[xname[0]] = {}; }
obj.renderPages[xname[0]][xname[1]] = obj.path.join(translateFolder, name);
if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
}
}
}
// See if there are any custom rending pages that will override the default ones
if ((obj.parent.webViewsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webViewsOverridePath, 'translations')))) {
translateFolder = obj.path.join(obj.parent.webViewsOverridePath, 'translations');
var files = obj.fs.readdirSync(translateFolder);
for (var i in files) {
var name = files[i];
if (name.endsWith('.handlebars')) {
name = name.substring(0, name.length - 11);
var xname = name.split('_');
if (xname.length == 2) {
if (obj.renderPages[xname[0]] == null) { obj.renderPages[xname[0]] = {}; }
obj.renderPages[xname[0]][xname[1]] = obj.path.join(translateFolder, name);
if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
}
}
}
}
}
}
// Get the list of pages with different languages that can be rendered
function getEmailLanguageList() {
// Fetch default rendeing pages
var translateFolder = null;
if (obj.fs.existsSync('emails/translations')) { translateFolder = 'emails/translations'; }
if (obj.fs.existsSync(obj.path.join(__dirname, 'emails', 'translations'))) { translateFolder = obj.path.join(__dirname, 'emails', 'translations'); }
if (translateFolder != null) {
obj.emailLanguages = ['en'];
var files = obj.fs.readdirSync(translateFolder);
for (var i in files) {
var name = files[i];
if (name.endsWith('.html')) {
name = name.substring(0, name.length - 5);
var xname = name.split('_');
if (xname.length == 2) {
if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
}
}
}
// See if there are any custom rending pages that will override the default ones
if ((obj.parent.webEmailsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webEmailsOverridePath, 'translations')))) {
translateFolder = obj.path.join(obj.parent.webEmailsOverridePath, 'translations');
var files = obj.fs.readdirSync(translateFolder);
for (var i in files) {
var name = files[i];
if (name.endsWith('.html')) {
name = name.substring(0, name.length - 5);
var xname = name.split('_');
if (xname.length == 2) {
if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
}
}
}
}
}
}
// Perform a web push to a user
// If any of the push fail, remove the subscription from the user's webpush subscription list.
obj.performWebPush = function (domain, user, payload, options) {
if ((parent.webpush == null) || (Array.isArray(user.webpush) == false) || (user.webpush.length == 0)) return;
var completionFunc = function pushCompletionFunc(sub, fail) {
pushCompletionFunc.failCount += fail;
if (--pushCompletionFunc.pushCount == 0) {
if (pushCompletionFunc.failCount > 0) {
var user = pushCompletionFunc.user, newwebpush = [];
for (var i in user.webpush) { if (user.webpush[i].fail == null) { newwebpush.push(user.webpush[i]); } }
user.webpush = newwebpush;
// Update the database
obj.db.SetUser(user);
// Event the change
var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
parent.DispatchEvent(targets, obj, message);
}
}
}
completionFunc.pushCount = user.webpush.length;
completionFunc.user = user;
completionFunc.domain = domain;
completionFunc.failCount = 0;
for (var i in user.webpush) {
var errorFunc = function pushErrorFunc(error) { pushErrorFunc.sub.fail = 1; pushErrorFunc.call(pushErrorFunc.sub, 1); }
errorFunc.sub = user.webpush[i];
errorFunc.call = completionFunc;
var successFunc = function pushSuccessFunc(value) { pushSuccessFunc.call(pushSuccessFunc.sub, 0); }
successFunc.sub = user.webpush[i];
successFunc.call = completionFunc;
parent.webpush.sendNotification(user.webpush[i], JSON.stringify(payload), options).then(successFunc, errorFunc);
}
}
// Ensure exclusivity of a push messaging token for Android device
obj.removePmtFromAllOtherNodes = function (node) {
if (typeof node.pmt != 'string') return;
db.Get('pmt_' + node.pmt, function (err, docs) {
if ((err == null) && (docs.length == 1)) {
var oldNodeId = docs[0].nodeid;
db.Get(oldNodeId, function (nerr, ndocs) {
if ((nerr == null) && (ndocs.length == 1)) {
var oldNode = ndocs[0];
if (oldNode.pmt == node.pmt) {
// Remove the push messaging token and save the node.
delete oldNode.pmt;
db.Set(oldNode);
// Event the node change
var event = { etype: 'node', action: 'changenode', nodeid: oldNode._id, domain: oldNode.domain, node: obj.CloneSafeNode(oldNode) }
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
parent.DispatchEvent(['*', oldNode.meshid, oldNode._id], obj, event);
}
}
});
}
db.Set({ _id: 'pmt_' + node.pmt, type: 'pmt', domain: node.domain, time: Date.now(), nodeid: node._id })
});
}
// Return true if a mobile browser is detected.
// This code comes from "http://detectmobilebrowsers.com/" and was modified, This is free and unencumbered software released into the public domain. For more information, please refer to the http://unlicense.org/
function isMobileBrowser(req) {
//var ua = req.headers['user-agent'].toLowerCase();
//return (/(android|bb\d+|meego).+mobile|mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(ua) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(ua.substr(0, 4)));
if (typeof req.headers['user-agent'] != 'string') return false;
return (req.headers['user-agent'].toLowerCase().indexOf('mobile') >= 0);
}
// Return decoded user agent information
obj.getUserAgentInfo = function (req) {
var browser = 'Unknown', os = 'Unknown';
try {
const ua = obj.uaparser((typeof req == 'string') ? req : req.headers['user-agent']);
if (ua.browser && ua.browser.name) { ua.browserStr = ua.browser.name; if (ua.browser.version) { ua.browserStr += '/' + ua.browser.version } }
if (ua.os && ua.os.name) { ua.osStr = ua.os.name; if (ua.os.version) { ua.osStr += '/' + ua.os.version } }
return ua;
} catch (ex) { return { browserStr: browser, osStr: os } }
}
// Return the query string portion of the URL, the ? and anything after.
function getQueryPortion(req) { var s = req.url.indexOf('?'); if (s == -1) { if (req.body && req.body.urlargs) { return req.body.urlargs; } return ''; } return req.url.substring(s); }
// Generate a random Intel AMT password
function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
function getRandomAmtPassword() { var p; do { p = Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
function getRandomLowerCase(len) { var r = '', random = obj.crypto.randomBytes(len); for (var i = 0; i < len; i++) { r += String.fromCharCode(97 + (random[i] % 26)); } return r; }
// Generate a 8 digit integer with even random probability for each value.
function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
// Clean a IPv6 address that encodes a IPv4 address
function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
// Set the content disposition header for a HTTP response.
// Because the filename can't have any special characters in it, we need to be extra careful.
function setContentDispositionHeader(res, type, name, size, altname) {
var name = require('path').basename(name).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split('\'').join('');
try {
var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(name) + '"' };
if (typeof size == 'number') { x['Content-Length'] = size; }
res.set(x);
} catch (ex) {
var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
if (typeof size == 'number') { x['Content-Length'] = size; }
res.set(x);
}
}
// Record a new entry in a recording log
function recordingEntry(fd, type, flags, data, func, tag) {
try {
if (typeof data == 'string') {
// String write
var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User)
header.writeInt32BE(blockData.length, 4); // Size
header.writeIntBE(new Date(), 10, 6); // Time
var block = Buffer.concat([header, blockData]);
obj.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
} else {
// Binary write
var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User)
header.writeInt32BE(data.length, 4); // Size
header.writeIntBE(new Date(), 10, 6); // Time
var block = Buffer.concat([header, data]);
obj.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
}
} catch (ex) { console.log(ex); func(fd, tag); }
}
// Perform a IP match against a list
function isIPMatch(ip, matchList) {
const ipcheck = require('ipcheck');
for (var i in matchList) { if (ipcheck.match(ip, matchList[i]) == true) return true; }
return false;
}
// This is the invalid login throttling code
obj.badLoginTable = {};
obj.badLoginTableLastClean = 0;
if (parent.config.settings == null) { parent.config.settings = {}; }
if (parent.config.settings.maxinvalidlogin !== false) {
if (typeof parent.config.settings.maxinvalidlogin != 'object') { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
if ((typeof parent.config.settings.maxinvalidlogin.coolofftime != 'number') || (parent.config.settings.maxinvalidlogin.coolofftime < 1)) { parent.config.settings.maxinvalidlogin.coolofftime = null; }
}
obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
if (parent.config.settings.maxinvalidlogin === false) return;
if (typeof ip == 'object') { ip = ip.clientIp; }
if (parent.config.settings.maxinvalidlogin != null) {
if (typeof parent.config.settings.maxinvalidlogin.exclude == 'string') {
const excludeSplit = parent.config.settings.maxinvalidlogin.exclude.split(',');
for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
} else if (Array.isArray(parent.config.settings.maxinvalidlogin.exclude)) {
for (var i in parent.config.settings.maxinvalidlogin.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalidlogin.exclude[i])) return; }
}
}
var splitip = ip.split('.');
if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
if (typeof obj.badLoginTable[ip] == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return; } } // Check cooloff period
if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
if ((obj.badLoginTable[ip].length >= parent.config.settings.maxinvalidlogin.count) && (parent.config.settings.maxinvalidlogin.coolofftime != null)) {
obj.badLoginTable[ip] = Date.now() + (parent.config.settings.maxinvalidlogin.coolofftime * 60000); // Move to cooloff period
}
}
obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
if (parent.config.settings.maxinvalidlogin === false) return true;
if (typeof ip == 'object') { ip = ip.clientIp; }
var splitip = ip.split('.');
if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
var ipTable = obj.badLoginTable[ip];
if (ipTable == null) return true;
if (typeof ipTable == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return false; } } // Check cooloff period
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
}
obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
if (parent.config.settings.maxinvalidlogin === false) return;
var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
for (var ip in obj.badLoginTable) {
var ipTable = obj.badLoginTable[ip];
if (typeof ipTable == 'number') {
if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } // Check cooloff period
} else {
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
}
}
obj.badLoginTableLastClean = 0;
}
// This is the invalid 2FA throttling code
obj.bad2faTable = {};
obj.bad2faTableLastClean = 0;
if (parent.config.settings == null) { parent.config.settings = {}; }
if (parent.config.settings.maxinvalid2fa !== false) {
if (typeof parent.config.settings.maxinvalid2fa != 'object') { parent.config.settings.maxinvalid2fa = { time: 10, count: 10 }; }
if (typeof parent.config.settings.maxinvalid2fa.time != 'number') { parent.config.settings.maxinvalid2fa.time = 10; }
if (typeof parent.config.settings.maxinvalid2fa.count != 'number') { parent.config.settings.maxinvalid2fa.count = 10; }
if ((typeof parent.config.settings.maxinvalid2fa.coolofftime != 'number') || (parent.config.settings.maxinvalid2fa.coolofftime < 1)) { parent.config.settings.maxinvalid2fa.coolofftime = null; }
}
obj.setbad2Fa = function (ip) { // Set an IP address that just did a bad 2FA request
if (parent.config.settings.maxinvalid2fa === false) return;
if (typeof ip == 'object') { ip = ip.clientIp; }
if (parent.config.settings.maxinvalid2fa != null) {
if (typeof parent.config.settings.maxinvalid2fa.exclude == 'string') {
const excludeSplit = parent.config.settings.maxinvalid2fa.exclude.split(',');
for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
} else if (Array.isArray(parent.config.settings.maxinvalid2fa.exclude)) {
for (var i in parent.config.settings.maxinvalid2fa.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalid2fa.exclude[i])) return; }
}
}
var splitip = ip.split('.');
if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
if (++obj.bad2faTableLastClean > 100) { obj.cleanBad2faTable(); }
if (typeof obj.bad2faTable[ip] == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return; } } // Check cooloff period
if (obj.bad2faTable[ip] == null) { obj.bad2faTable[ip] = [Date.now()]; } else { obj.bad2faTable[ip].push(Date.now()); }
if ((obj.bad2faTable[ip].length >= parent.config.settings.maxinvalid2fa.count) && (parent.config.settings.maxinvalid2fa.coolofftime != null)) {
obj.bad2faTable[ip] = Date.now() + (parent.config.settings.maxinvalid2fa.coolofftime * 60000); // Move to cooloff period
}
}
obj.checkAllow2Fa = function (ip) { // Check if an IP address is allowed to perform 2FA
if (parent.config.settings.maxinvalid2fa === false) return true;
if (typeof ip == 'object') { ip = ip.clientIp; }
var splitip = ip.split('.');
if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
var ipTable = obj.bad2faTable[ip];
if (ipTable == null) return true;
if (typeof ipTable == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return false; } } // Check cooloff period
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.bad2faTable[ip]; return true; }
return (ipTable.length < parent.config.settings.maxinvalid2fa.count); // No more than x bad 2FAs in x minutes
}
obj.cleanBad2faTable = function () { // Clean up the IP address 2FA blockage table, we do this occasionaly.
if (parent.config.settings.maxinvalid2fa === false) return;
var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
for (var ip in obj.bad2faTable) {
var ipTable = obj.bad2faTable[ip];
if (typeof ipTable == 'number') {
if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } // Check cooloff period
} else {
while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
if (ipTable.length == 0) { delete obj.bad2faTable[ip]; }
}
}
obj.bad2faTableLastClean = 0;
}
// Hold a websocket until additional arguments are provided within the socket.
// This is a generic function that can be used for any websocket to avoid passing arguments in the URL.
function getWebsocketArgs(ws, req, func) {
if (req.query.moreargs != '1') {
// No more arguments needed, pass the websocket thru
func(ws, req);
} else {
// More arguments are needed
delete req.query.moreargs;
const xfunc = function getWebsocketArgsEx(msg) {
var command = null;
try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
if ((command != null) && (command.action === 'urlargs') && (typeof command.args == 'object')) {
for (var i in command.args) { getWebsocketArgsEx.req.query[i] = command.args[i]; }
ws.removeEventListener('message', getWebsocketArgsEx);
getWebsocketArgsEx.func(getWebsocketArgsEx.ws, getWebsocketArgsEx.req);
}
}
xfunc.ws = ws;
xfunc.req = req;
xfunc.func = func;
ws.on('message', xfunc);
}
}
// Set a random value to this session. Only works if the session has a userid.
// This random value along with the userid is used to destroy the session when logging out.
function setSessionRandom(req) {
if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return;
var x = obj.crypto.randomBytes(6).toString('base64');
while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); }
req.session.x = x;
}
// Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway.
function clearDestroyedSessions() {
var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000);
for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } }
for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; }
}
// Check and/or convert the agent color value into a correct string or return empty string.
function checkAgentColorString(header, value) {
if ((typeof header !== 'string') || (typeof value !== 'string')) return '';
if (value.startsWith('#') && (value.length == 7)) {
// Convert color in hex format
value = parseInt(value.substring(1, 3), 16) + ',' + parseInt(value.substring(3, 5), 16) + ',' + parseInt(value.substring(5, 7), 16);
} else {
// Check color in decimal format
const valueSplit = value.split(',');
if (valueSplit.length != 3) return '';
const r = parseInt(valueSplit[0]), g = parseInt(valueSplit[1]), b = parseInt(valueSplit[2]);
if (isNaN(r) || (r < 0) || (r > 255) || isNaN(g) || (g < 0) || (g > 255) || isNaN(b) || (b < 0) || (b > 255)) return '';
value = r + ',' + g + ',' + b;
}
return header + value + '\r\n';
}
// Check that everything is cleaned up
function checkWebRelaySessionsTimeout() {
for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
}
// Return true if this is a private IP address
function isPrivateAddress(ip_addr) {
// If this is a loopback address, return true
if ((ip_addr == '127.0.0.1') || (ip_addr == '::1')) return true;
// Check IPv4 private addresses
const ipcheck = require('ipcheck');
const IPv4PrivateRanges = ['0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24', '192.0.0.0/29', '192.0.0.8/32', '192.0.0.9/32', '192.0.0.10/32', '192.0.0.170/32', '192.0.0.171/32', '192.0.2.0/24', '192.31.196.0/24', '192.52.193.0/24', '192.88.99.0/24', '192.168.0.0/16', '192.175.48.0/24', '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24', '240.0.0.0/4', '255.255.255.255/32']
for (var i in IPv4PrivateRanges) { if (ipcheck.match(ip_addr, IPv4PrivateRanges[i])) return true; }
// Check IPv6 private addresses
return /^::$/.test(ip_addr) ||
/^::1$/.test(ip_addr) ||
/^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
/^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
/^64:ff9b::([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
/^100::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
/^2001::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
/^2001:2[0-9a-fA-F]:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
/^2001:db8:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
/^2002:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
/^f[c-d]([0-9a-fA-F]{2,2}):/i.test(ip_addr) ||
/^fe[8-9a-bA-B][0-9a-fA-F]:/i.test(ip_addr) ||
/^ff([0-9a-fA-F]{2,2}):/i.test(ip_addr)
}
// Check that a cookie IP is within the correct range depending on the active policy
function checkCookieIp(cookieip, ip) {
if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking
if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers.
if (require('ipcheck').match(cookieip, ip + '/24')) return true; // 'lax' - IP address need to be in the some range
return (isPrivateAddress(cookieip) && isPrivateAddress(ip)); // 'lax' - If both IP addresses are private or loopback, accept it. This is needed because sometimes browsers will resolve IP addresses oddly on private networks.
}
// Takes a formating string like "this {{{a}}} is an {{{b}}} example" and fills the a and b with input o.a and o.b
function assembleStringFromObject(format, o) {
var r = '', i = format.indexOf('{{{');
if (i > 0) { r = format.substring(0, i); format = format.substring(i); }
const cmd = format.split('{{{');
for (var j in cmd) { if (j == 0) continue; i = cmd[j].indexOf('}}}'); r += o[cmd[j].substring(0, i)] + cmd[j].substring(i + 3); }
return r;
}
// Sync an account with an external user group.
// Return true if the user was changed
function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) {
var userChanged = false;
if (user.links == null) { user.links = {}; }
// Create a user of memberships for this user that type
var existingUserMemberships = {};
for (var i in user.links) {
if (i.startsWith('ugrp/') && (obj.userGroups[i] != null) && (obj.userGroups[i].membershipType == userMembershipType)) { existingUserMemberships[i] = obj.userGroups[i]; }
}
// Go thru the list user memberships and create and add to any user groups as needed
for (var i in userMemberships) {
const membership = userMemberships[i];
var ugrpid = 'ugrp/' + domain.id + '/' + obj.crypto.createHash('sha384').update(membership).digest('base64').replace(/\+/g, '@').replace(/\//g, '$');
var ugrp = obj.userGroups[ugrpid];
if (ugrp == null) {
// This user group does not exist, create it
ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} };
// Save the new group
db.Set(ugrp);
if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; }
// Event the user group creation
var event = { etype: 'ugrp', ugrpid: ugrpid, name: ugrp.name, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id };
parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
// Log in the auth log
parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
}
if (existingUserMemberships[ugrpid] == null) {
// This user is not part of the user group, add it.
if (user.links == null) { user.links = {}; }
user.links[ugrp._id] = { rights: 1 };
userChanged = true;
db.SetUser(user);
parent.DispatchEvent([user._id], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', user._id];
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
parent.DispatchEvent(targets, obj, event);
// Add a user to the user group
ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
db.Set(ugrp);
// Notify user group change
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 71, msgArgs: [user.name, ugrp.name], msg: 'Added user(s) ' + user.name + ' to user group ' + ugrp.name, addUserDomain: domain.id };
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(['*', ugrp._id, user._id], obj, event);
// Log in the auth log
parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
} else {
// User is already part of this user group
delete existingUserMemberships[ugrpid];
}
}
// Remove the user from any memberships they don't belong to anymore
for (var ugrpid in existingUserMemberships) {
var ugrp = obj.userGroups[ugrpid];
parent.authLog('https', userMembershipType.toUpperCase() + ': Removing ' + user.name + ' from user group ' + ugrp.name + '.');
if ((user.links != null) && (user.links[ugrpid] != null)) {
delete user.links[ugrpid];
// Notify user change
var targets = ['*', 'server-users', user._id, user._id];
var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
parent.DispatchEvent(targets, obj, event);
db.SetUser(user);
parent.DispatchEvent([user._id], obj, 'resubscribe');
}
if (ugrp != null) {
// Remove the user from the group
if ((ugrp.links != null) && (ugrp.links[user._id] != null)) {
delete ugrp.links[user._id];
db.Set(ugrp);
// Notify user group change
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 72, msgArgs: [user.name, ugrp.name], msg: 'Removed user ' + user.name + ' from user group ' + ugrp.name, domain: domain.id };
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(['*', ugrp._id, user._id], obj, event);
}
}
}
return userChanged;
}
return obj;
};