Large file — syntax highlighting disabled.
| 1 | /** |
| 2 | * @description MeshCentral web server |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @copyright Intel Corporation 2018-2022 |
| 5 | * @license Apache-2.0 |
| 6 | * @version v0.0.1 |
| 7 | */ |
| 8 | |
| 9 | /*jslint node: true */ |
| 10 | /*jshint node: true */ |
| 11 | /*jshint strict:false */ |
| 12 | /*jshint -W097 */ |
| 13 | /*jshint esversion: 6 */ |
| 14 | 'use strict'; |
| 15 | |
| 16 | // SerialTunnel object is used to embed TLS within another connection. |
| 17 | function SerialTunnel(options) { |
| 18 | var obj = new require('stream').Duplex(options); |
| 19 | obj.forwardwrite = null; |
| 20 | obj.updateBuffer = function (chunk) { this.push(chunk); }; |
| 21 | 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 |
| 22 | obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer() |
| 23 | return obj; |
| 24 | } |
| 25 | |
| 26 | // ExpressJS login sample |
| 27 | // https://github.com/expressjs/express/blob/master/examples/auth/index.js |
| 28 | |
| 29 | // Polyfill startsWith/endsWith for older NodeJS |
| 30 | if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; } |
| 31 | 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; }; } |
| 32 | |
| 33 | // Construct a HTTP server object |
| 34 | module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) { |
| 35 | var obj = {}, i = 0; |
| 36 | |
| 37 | // Modules |
| 38 | obj.fs = require('fs'); |
| 39 | obj.net = require('net'); |
| 40 | obj.tls = require('tls'); |
| 41 | obj.path = require('path'); |
| 42 | obj.os = require('os'); |
| 43 | obj.bodyParser = require('body-parser'); |
| 44 | obj.exphbs = require('express-handlebars'); |
| 45 | obj.crypto = require('crypto'); |
| 46 | obj.common = require('./common.js'); |
| 47 | obj.express = require('express'); |
| 48 | obj.meshAgentHandler = require('./meshagent.js'); |
| 49 | obj.meshRelayHandler = require('./meshrelay.js'); |
| 50 | obj.meshDeviceFileHandler = require('./meshdevicefile.js'); |
| 51 | obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js'); |
| 52 | obj.meshIderHandler = require('./amt/amt-ider.js'); |
| 53 | obj.meshUserHandler = require('./meshuser.js'); |
| 54 | obj.interceptor = require('./interceptor'); |
| 55 | obj.uaparser = require('ua-parser-js'); |
| 56 | obj.uaclienthints = require('ua-client-hints-js'); |
| 57 | const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. |
| 58 | |
| 59 | // Setup WebAuthn / FIDO2 |
| 60 | obj.webauthn = require('./webauthn.js').CreateWebAuthnModule(); |
| 61 | |
| 62 | if (process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { |
| 63 | obj.httpsProxyAgent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); |
| 64 | } |
| 65 | |
| 66 | // Variables |
| 67 | obj.args = args; |
| 68 | obj.parent = parent; |
| 69 | obj.filespath = parent.filespath; |
| 70 | obj.db = db; |
| 71 | obj.app = obj.express(); |
| 72 | if (obj.args.agentport) { obj.agentapp = obj.express(); } |
| 73 | if (args.compression === true) { |
| 74 | obj.app.use(require('compression')({ filter: function (req, res) { |
| 75 | if (req.path == '/devicefile.ashx') return false; // Don't compress device file transfers to show file sizes |
| 76 | if ((args.relaydns != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) return false; // Don't compress DNS relay requests |
| 77 | return require('compression').filter(req, res); |
| 78 | }})); |
| 79 | } |
| 80 | obj.app.disable('x-powered-by'); |
| 81 | obj.tlsServer = null; |
| 82 | obj.tcpServer = null; |
| 83 | obj.certificates = certificates; |
| 84 | obj.users = {}; // UserID --> User |
| 85 | obj.meshes = {}; // MeshID --> Mesh (also called device group) |
| 86 | obj.userGroups = {}; // UGrpID --> User Group |
| 87 | obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node |
| 88 | obj.tlsCiphers = args.tlsciphers; // List of TLS ciphers to use |
| 89 | obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users |
| 90 | obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents |
| 91 | obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents |
| 92 | obj.tlsSniCredentials = null; |
| 93 | obj.dnsDomains = {}; |
| 94 | obj.relaySessionCount = 0; |
| 95 | obj.relaySessionErrorCount = 0; |
| 96 | obj.blockedUsers = 0; |
| 97 | obj.blockedAgents = 0; |
| 98 | obj.renderPages = null; |
| 99 | obj.renderLanguages = []; |
| 100 | obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time |
| 101 | |
| 102 | const isWindowsPlatform = (obj.os.platform() === 'win32'); |
| 103 | const safeUploadTempRoots = (function () { |
| 104 | const roots = []; |
| 105 | const addRoot = function (p) { |
| 106 | if (typeof p !== 'string') { return; } |
| 107 | var resolved; |
| 108 | try { resolved = obj.path.normalize(obj.path.resolve(p)); } catch (ex) { return; } |
| 109 | if (resolved.length === 0) { return; } |
| 110 | if ((resolved.length > 1) && resolved.endsWith(obj.path.sep)) { resolved = resolved.slice(0, -1); } |
| 111 | const comparison = isWindowsPlatform ? resolved.toLowerCase() : resolved; |
| 112 | const comparisonWithSep = comparison + obj.path.sep; |
| 113 | roots.push({ comparison: comparison, comparisonWithSep: comparisonWithSep }); |
| 114 | }; |
| 115 | addRoot(obj.os.tmpdir()); |
| 116 | if (typeof obj.parent.filespath === 'string') { addRoot(obj.path.join(obj.parent.filespath, 'tmp')); } |
| 117 | return roots; |
| 118 | })(); |
| 119 | function resolveSafeUploadTempPath(tempPath) { |
| 120 | if (typeof tempPath !== 'string') { return null; } |
| 121 | var resolvedPath; |
| 122 | try { resolvedPath = obj.path.normalize(obj.path.resolve(tempPath)); } catch (ex) { return null; } |
| 123 | var comparisonPath = isWindowsPlatform ? resolvedPath.toLowerCase() : resolvedPath; |
| 124 | var comparisonPathNoTrailing = comparisonPath; |
| 125 | if ((comparisonPathNoTrailing.length > 1) && comparisonPathNoTrailing.endsWith(obj.path.sep)) { comparisonPathNoTrailing = comparisonPathNoTrailing.slice(0, -1); } |
| 126 | for (var i = 0; i < safeUploadTempRoots.length; i++) { |
| 127 | var root = safeUploadTempRoots[i]; |
| 128 | if ((comparisonPathNoTrailing === root.comparison) || comparisonPath.startsWith(root.comparisonWithSep)) { return resolvedPath; } |
| 129 | } |
| 130 | return null; |
| 131 | } |
| 132 | |
| 133 | // Web relay sessions |
| 134 | var webRelayNextSessionId = 1; |
| 135 | var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session |
| 136 | var webRelayCleanupTimer = null; |
| 137 | |
| 138 | // Monitor web relay session removals |
| 139 | parent.AddEventDispatch(['server-shareremove'], obj); |
| 140 | obj.HandleEvent = function (source, event, ids, id) { |
| 141 | if (event.action == 'removedDeviceShare') { |
| 142 | for (var relaySessionId in webRelaySessions) { |
| 143 | // A share was removed that matches an active session, close the web relay session. |
| 144 | if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); } |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // Mesh Rights |
| 150 | const MESHRIGHT_EDITMESH = 0x00000001; |
| 151 | const MESHRIGHT_MANAGEUSERS = 0x00000002; |
| 152 | const MESHRIGHT_MANAGECOMPUTERS = 0x00000004; |
| 153 | const MESHRIGHT_REMOTECONTROL = 0x00000008; |
| 154 | const MESHRIGHT_AGENTCONSOLE = 0x00000010; |
| 155 | const MESHRIGHT_SERVERFILES = 0x00000020; |
| 156 | const MESHRIGHT_WAKEDEVICE = 0x00000040; |
| 157 | const MESHRIGHT_SETNOTES = 0x00000080; |
| 158 | const MESHRIGHT_REMOTEVIEWONLY = 0x00000100; |
| 159 | const MESHRIGHT_NOTERMINAL = 0x00000200; |
| 160 | const MESHRIGHT_NOFILES = 0x00000400; |
| 161 | const MESHRIGHT_NOAMT = 0x00000800; |
| 162 | const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000; |
| 163 | const MESHRIGHT_LIMITEVENTS = 0x00002000; |
| 164 | const MESHRIGHT_CHATNOTIFY = 0x00004000; |
| 165 | const MESHRIGHT_UNINSTALL = 0x00008000; |
| 166 | const MESHRIGHT_NODESKTOP = 0x00010000; |
| 167 | const MESHRIGHT_REMOTECOMMAND = 0x00020000; |
| 168 | const MESHRIGHT_RESETOFF = 0x00040000; |
| 169 | const MESHRIGHT_GUESTSHARING = 0x00080000; |
| 170 | const MESHRIGHT_ADMIN = 0xFFFFFFFF; |
| 171 | |
| 172 | // Site rights |
| 173 | const SITERIGHT_SERVERBACKUP = 0x00000001; |
| 174 | const SITERIGHT_MANAGEUSERS = 0x00000002; |
| 175 | const SITERIGHT_SERVERRESTORE = 0x00000004; |
| 176 | const SITERIGHT_FILEACCESS = 0x00000008; |
| 177 | const SITERIGHT_SERVERUPDATE = 0x00000010; |
| 178 | const SITERIGHT_LOCKED = 0x00000020; |
| 179 | const SITERIGHT_NONEWGROUPS = 0x00000040; |
| 180 | const SITERIGHT_NOMESHCMD = 0x00000080; |
| 181 | const SITERIGHT_USERGROUPS = 0x00000100; |
| 182 | const SITERIGHT_RECORDINGS = 0x00000200; |
| 183 | const SITERIGHT_LOCKSETTINGS = 0x00000400; |
| 184 | const SITERIGHT_ALLEVENTS = 0x00000800; |
| 185 | const SITERIGHT_NONEWDEVICES = 0x00001000; |
| 186 | const SITERIGHT_ADMIN = 0xFFFFFFFF; |
| 187 | |
| 188 | // Setup SSPI authentication if needed |
| 189 | if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) { |
| 190 | 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 }); } } |
| 191 | } |
| 192 | |
| 193 | // Perform hash on web certificate and agent certificate |
| 194 | obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert); |
| 195 | obj.webCertificateHashs = { '': obj.webCertificateHash }; |
| 196 | obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); |
| 197 | obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert); |
| 198 | obj.webCertificateFullHashs = { '': obj.webCertificateFullHash }; |
| 199 | obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) }; |
| 200 | obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert); |
| 201 | obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); |
| 202 | obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes(); |
| 203 | obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null; |
| 204 | obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null; |
| 205 | |
| 206 | // Compute the hash of all of the web certificates for each domain |
| 207 | for (var i in obj.parent.config.domains) { |
| 208 | if (obj.parent.config.domains[i].certhash != null) { |
| 209 | // If the web certificate hash is provided, use it. |
| 210 | obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary'); |
| 211 | if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); } |
| 212 | delete obj.webCertificateExpire[i]; // Expire time is not provided |
| 213 | } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) { |
| 214 | // If the domain has a different DNS name, use a different certificate hash. |
| 215 | // Hash the full certificate |
| 216 | obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert); |
| 217 | obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter); |
| 218 | try { |
| 219 | // Decode a RSA certificate and hash the public key. |
| 220 | obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert); |
| 221 | } catch (ex) { |
| 222 | // This may be a ECDSA certificate, hash the entire cert. |
| 223 | obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i]; |
| 224 | } |
| 225 | } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) { |
| 226 | // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs. |
| 227 | obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert); |
| 228 | obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert); |
| 229 | obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter); |
| 230 | } else if (i != '') { |
| 231 | // For any other domain, use the default cert. |
| 232 | obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs['']; |
| 233 | obj.webCertificateHashs[i] = obj.webCertificateHashs['']; |
| 234 | obj.webCertificateExpire[i] = obj.webCertificateExpire['']; |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // If we are running the legacy swarm server, compute the hash for that certificate |
| 239 | if (parent.certificates.swarmserver != null) { |
| 240 | obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes(); |
| 241 | 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' }); |
| 242 | 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' }); |
| 243 | } |
| 244 | |
| 245 | // Main lists |
| 246 | obj.wsagents = {}; // NodeId --> Agent |
| 247 | obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent |
| 248 | obj.wsagentsDisconnections = {}; |
| 249 | obj.wsagentsDisconnectionsTimer = null; |
| 250 | obj.duplicateAgentsLog = {}; |
| 251 | obj.wssessions = {}; // UserId --> Array Of Sessions |
| 252 | obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd) |
| 253 | obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd" |
| 254 | obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId |
| 255 | obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ] |
| 256 | obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount |
| 257 | obj.wsrelays = {}; // Id -> Relay |
| 258 | obj.desktoprelays = {}; // Id -> Desktop Multiplexer Relay |
| 259 | obj.wsPeerRelays = {}; // Id -> { ServerId, Time } |
| 260 | var tlsSessionStore = {}; // Store TLS session information for quick resume. |
| 261 | var tlsSessionStoreCount = 0; // Number of cached TLS session information in store. |
| 262 | |
| 263 | // Setup randoms |
| 264 | obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; }); |
| 265 | obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); }); |
| 266 | obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; }); |
| 267 | |
| 268 | // Get non-english web pages and emails |
| 269 | getRenderList(); |
| 270 | getEmailLanguageList(); |
| 271 | |
| 272 | // Setup DNS domain TLS SNI credentials |
| 273 | { |
| 274 | var dnscount = 0; |
| 275 | obj.tlsSniCredentials = {}; |
| 276 | 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++; } } |
| 277 | 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; } |
| 278 | } |
| 279 | function TlsSniCallback(name, cb) { |
| 280 | var c = obj.tlsSniCredentials[name]; |
| 281 | if (c != null) { |
| 282 | cb(null, c); |
| 283 | } else { |
| 284 | cb(null, obj.tlsSniCredentials['']); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; } |
| 289 | //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; } |
| 290 | // Fetch all users from the database, keep this in memory |
| 291 | obj.db.GetAllType('user', function (err, docs) { |
| 292 | obj.common.unEscapeAllLinksFieldName(docs); |
| 293 | var domainUserCount = {}, i = 0; |
| 294 | for (i in parent.config.domains) { domainUserCount[i] = 0; } |
| 295 | for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; } |
| 296 | for (i in parent.config.domains) { |
| 297 | if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) { |
| 298 | // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation. |
| 299 | //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; } |
| 300 | console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.'); |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | // Fetch all device groups (meshes) from the database, keep this in memory |
| 305 | // As we load things in memory, we will also be doing some cleaning up. |
| 306 | // We will not save any clean up in the database right now, instead it will be saved next time there is a change. |
| 307 | obj.db.GetAllType('mesh', function (err, docs) { |
| 308 | obj.common.unEscapeAllLinksFieldName(docs); |
| 309 | for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones. |
| 310 | |
| 311 | // Fetch all user groups from the database, keep this in memory |
| 312 | obj.db.GetAllType('ugrp', function (err, docs) { |
| 313 | obj.common.unEscapeAllLinksFieldName(docs); |
| 314 | |
| 315 | // Perform user group link cleanup |
| 316 | for (var i in docs) { |
| 317 | const ugrp = docs[i]; |
| 318 | if (ugrp.links != null) { |
| 319 | for (var j in ugrp.links) { |
| 320 | if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist |
| 321 | 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 |
| 322 | } |
| 323 | } |
| 324 | obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups |
| 325 | } |
| 326 | |
| 327 | // Mapping between users and groups |
| 328 | for (var ugrpId in obj.userGroups) { |
| 329 | const ugrp = obj.userGroups[ugrpId]; |
| 330 | if (ugrp.links != null) { |
| 331 | for (var userId in ugrp.links) { |
| 332 | if (userId.startsWith('user/') && (obj.users[userId] != null)) { |
| 333 | const user = obj.users[userId]; |
| 334 | if (user.links == null) { user.links = {}; } |
| 335 | if (user.links[ugrpId] == null) { |
| 336 | // Adding group link to user |
| 337 | user.links[ugrpId] = { rights: ugrp.links[userId].rights || 1 }; |
| 338 | } |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | // Perform device group link cleanup |
| 345 | for (var i in obj.meshes) { |
| 346 | const mesh = obj.meshes[i]; |
| 347 | if (mesh.links != null) { |
| 348 | for (var j in mesh.links) { |
| 349 | 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 |
| 350 | 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 |
| 351 | } |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | // Perform user link cleanup |
| 356 | for (var i in obj.users) { |
| 357 | const user = obj.users[i]; |
| 358 | if (user.links != null) { |
| 359 | for (var j in user.links) { |
| 360 | if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist |
| 361 | 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 |
| 362 | //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO |
| 363 | } |
| 364 | //if (Object.keys(user.links).length == 0) { delete user.links; } |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | // We loaded the users, device groups and user group state, start the server |
| 369 | serverStart(); |
| 370 | }); |
| 371 | }); |
| 372 | }); |
| 373 | |
| 374 | // Clean up a device, used before saving it in the database |
| 375 | obj.cleanDevice = function (device) { |
| 376 | // Check device links, if a link points to an unknown user, remove it. |
| 377 | if (device.links != null) { |
| 378 | for (var j in device.links) { |
| 379 | if ((obj.users[j] == null) && (obj.userGroups[j] == null)) { |
| 380 | delete device.links[j]; |
| 381 | if (Object.keys(device.links).length == 0) { delete device.links; } |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | return device; |
| 386 | } |
| 387 | |
| 388 | // Return statistics about this web server |
| 389 | obj.getStats = function () { |
| 390 | return { |
| 391 | users: Object.keys(obj.users).length, |
| 392 | meshes: Object.keys(obj.meshes).length, |
| 393 | dnsDomains: Object.keys(obj.dnsDomains).length, |
| 394 | relaySessionCount: obj.relaySessionCount, |
| 395 | relaySessionErrorCount: obj.relaySessionErrorCount, |
| 396 | wsagents: Object.keys(obj.wsagents).length, |
| 397 | wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length, |
| 398 | wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length, |
| 399 | wssessions: Object.keys(obj.wssessions).length, |
| 400 | wssessions2: Object.keys(obj.wssessions2).length, |
| 401 | wsPeerSessions: Object.keys(obj.wsPeerSessions).length, |
| 402 | wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length, |
| 403 | wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length, |
| 404 | sessionsCount: Object.keys(obj.sessionsCount).length, |
| 405 | wsrelays: Object.keys(obj.wsrelays).length, |
| 406 | wsPeerRelays: Object.keys(obj.wsPeerRelays).length, |
| 407 | tlsSessionStore: Object.keys(tlsSessionStore).length, |
| 408 | blockedUsers: obj.blockedUsers, |
| 409 | blockedAgents: obj.blockedAgents |
| 410 | }; |
| 411 | } |
| 412 | |
| 413 | // Agent counters |
| 414 | obj.agentStats = { |
| 415 | createMeshAgentCount: 0, |
| 416 | agentClose: 0, |
| 417 | agentBinaryUpdate: 0, |
| 418 | agentMeshCoreBinaryUpdate: 0, |
| 419 | coreIsStableCount: 0, |
| 420 | verifiedAgentConnectionCount: 0, |
| 421 | clearingCoreCount: 0, |
| 422 | updatingCoreCount: 0, |
| 423 | recoveryCoreIsStableCount: 0, |
| 424 | meshDoesNotExistCount: 0, |
| 425 | invalidPkcsSignatureCount: 0, |
| 426 | invalidRsaSignatureCount: 0, |
| 427 | invalidJsonCount: 0, |
| 428 | unknownAgentActionCount: 0, |
| 429 | agentBadWebCertHashCount: 0, |
| 430 | agentBadSignature1Count: 0, |
| 431 | agentBadSignature2Count: 0, |
| 432 | agentMaxSessionHoldCount: 0, |
| 433 | invalidDomainMeshCount: 0, |
| 434 | invalidMeshTypeCount: 0, |
| 435 | invalidDomainMesh2Count: 0, |
| 436 | invalidMeshType2Count: 0, |
| 437 | duplicateAgentCount: 0, |
| 438 | maxDomainDevicesReached: 0, |
| 439 | agentInTrouble: 0, |
| 440 | agentInBigTrouble: 0 |
| 441 | } |
| 442 | obj.getAgentStats = function () { return obj.agentStats; } |
| 443 | |
| 444 | // Traffic counters |
| 445 | obj.trafficStats = { |
| 446 | httpRequestCount: 0, |
| 447 | httpWebSocketCount: 0, |
| 448 | httpIn: 0, |
| 449 | httpOut: 0, |
| 450 | relayCount: {}, |
| 451 | relayIn: {}, |
| 452 | relayOut: {}, |
| 453 | localRelayCount: {}, |
| 454 | localRelayIn: {}, |
| 455 | localRelayOut: {}, |
| 456 | AgentCtrlIn: 0, |
| 457 | AgentCtrlOut: 0, |
| 458 | LMSIn: 0, |
| 459 | LMSOut: 0, |
| 460 | CIRAIn: 0, |
| 461 | CIRAOut: 0 |
| 462 | } |
| 463 | obj.trafficStats.time = Date.now(); |
| 464 | obj.getTrafficStats = function () { return obj.trafficStats; } |
| 465 | obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time. |
| 466 | const data = obj.common.Clone(obj.trafficStats); |
| 467 | data.time = Date.now(); |
| 468 | const delta = calcDelta(oldTraffic ? oldTraffic : {}, data); |
| 469 | if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); } |
| 470 | delta.time = data.time; |
| 471 | return { current: data, delta: delta } |
| 472 | } |
| 473 | function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers |
| 474 | const r = {}; |
| 475 | for (var i in newData) { |
| 476 | if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); } |
| 477 | if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } } |
| 478 | } |
| 479 | return r; |
| 480 | } |
| 481 | |
| 482 | // Keep a record of the last agent issues. |
| 483 | obj.getAgentIssues = function () { return obj.agentIssues; } |
| 484 | obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } } |
| 485 | obj.agentIssues = []; |
| 486 | |
| 487 | // Authenticate the user |
| 488 | obj.authenticate = function (name, pass, domain, fn) { |
| 489 | if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; } |
| 490 | if (name.startsWith('~t:')) { |
| 491 | // Login token, try to fetch the token from the database |
| 492 | obj.db.Get('logintoken-' + name, function (err, docs) { |
| 493 | if (err != null) { fn(err); return; } |
| 494 | if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; } |
| 495 | const loginToken = docs[0]; |
| 496 | if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; } |
| 497 | |
| 498 | // Default strong password hashing (pbkdf2 SHA384) |
| 499 | require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) { |
| 500 | if (err) return fn(err); |
| 501 | if (hash == loginToken.hash) { |
| 502 | // Login username and password are valid. |
| 503 | var user = obj.users[loginToken.userid]; |
| 504 | if (!user) { fn(new Error('cannot find user')); return; } |
| 505 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } |
| 506 | |
| 507 | // Successful login token authentication |
| 508 | var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser }; |
| 509 | if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; } |
| 510 | return fn(null, user._id, null, loginOptions); |
| 511 | } |
| 512 | fn(new Error('invalid password')); |
| 513 | }, 0); |
| 514 | }); |
| 515 | } else if (domain.auth == 'ldap') { |
| 516 | // This method will handle LDAP login |
| 517 | const ldapHandler = function ldapHandlerFunc(err, xxuser) { |
| 518 | 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; } |
| 519 | |
| 520 | // Save this LDAP user to file if needed |
| 521 | if (typeof domain.ldapsaveusertofile == 'string') { |
| 522 | obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { }); |
| 523 | } |
| 524 | |
| 525 | // Work on getting the userid for this LDAP user |
| 526 | var shortname = null; |
| 527 | var username = xxuser['displayName']; |
| 528 | if (typeof domain.ldapusername == 'string') { |
| 529 | if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; } |
| 530 | } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; } |
| 531 | if (domain.ldapuserbinarykey) { |
| 532 | // Use a binary key as the userid |
| 533 | if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); } |
| 534 | } else if (domain.ldapuserkey) { |
| 535 | // Use a string key as the userid |
| 536 | if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; } |
| 537 | } else { |
| 538 | // Use the default key as the userid |
| 539 | if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); } |
| 540 | else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); } |
| 541 | else if (xxuser['name']) { shortname = xxuser['name']; } |
| 542 | else if (xxuser['cn']) { shortname = xxuser['cn']; } |
| 543 | } |
| 544 | if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; } |
| 545 | if (username == null) { username = shortname; } |
| 546 | var userid = 'user/' + domain.id + '/' + shortname; |
| 547 | |
| 548 | // Get the list of groups this user is a member of. |
| 549 | var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf']; |
| 550 | if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; } |
| 551 | if (Array.isArray(userMemberships) == false) { userMemberships = []; } |
| 552 | |
| 553 | // See if the user is required to be part of an LDAP user group in order to log into this server. |
| 554 | if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; } |
| 555 | if (Array.isArray(domain.ldapuserrequiredgroupmembership)) { |
| 556 | // Look for a matching LDAP user group |
| 557 | var userMembershipMatch = false; |
| 558 | for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } } |
| 559 | 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 |
| 560 | } |
| 561 | |
| 562 | // Check if user is in an site administrator group |
| 563 | var siteAdminGroup = null; |
| 564 | if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; } |
| 565 | if (Array.isArray(domain.ldapsiteadmingroups)) { |
| 566 | siteAdminGroup = false; |
| 567 | for (var i in domain.ldapsiteadmingroups) { |
| 568 | if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; } |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | // See if we need to sync LDAP user memberships with user groups |
| 573 | if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; } |
| 574 | if (typeof domain.ldapsyncwithusergroups == 'object') { |
| 575 | // LDAP user memberships sync is enabled, see if there are any filters to apply |
| 576 | if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; } |
| 577 | if (Array.isArray(domain.ldapsyncwithusergroups.filter)) { |
| 578 | const g = []; |
| 579 | for (var i in userMemberships) { |
| 580 | var match = false; |
| 581 | for (var j in domain.ldapsyncwithusergroups.filter) { |
| 582 | if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; } |
| 583 | } |
| 584 | if (match) { g.push(userMemberships[i]); } |
| 585 | } |
| 586 | userMemberships = g; |
| 587 | } |
| 588 | } else { |
| 589 | // LDAP user memberships sync is disabled, sync the user with empty membership |
| 590 | userMemberships = []; |
| 591 | } |
| 592 | |
| 593 | // Get the email address for this LDAP user |
| 594 | var email = null; |
| 595 | if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default |
| 596 | if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value. |
| 597 | if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent. |
| 598 | |
| 599 | // Get the real name for this LDAP user |
| 600 | var realname = null; |
| 601 | if (typeof domain.ldapuserrealname == 'string') { |
| 602 | if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; } |
| 603 | } |
| 604 | else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } } |
| 605 | |
| 606 | // Get the phone number for this LDAP user |
| 607 | var phonenumber = null; |
| 608 | if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; } |
| 609 | else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } } |
| 610 | |
| 611 | // Work on getting the image of this LDAP user |
| 612 | var userimage = null, userImageBuffer = null; |
| 613 | if (xxuser._raw) { // Using _raw allows us to get data directly as buffer. |
| 614 | if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; } |
| 615 | else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; } |
| 616 | else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; } |
| 617 | if (userImageBuffer != null) { |
| 618 | if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); } |
| 619 | if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); } |
| 620 | } |
| 621 | } |
| 622 | |
| 623 | // Display user information extracted from LDAP data |
| 624 | parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null)); |
| 625 | |
| 626 | // If there is a testing userid, use that |
| 627 | if (ldapHandlerFunc.ldapShortName) { |
| 628 | shortname = ldapHandlerFunc.ldapShortName; |
| 629 | userid = 'user/' + domain.id + '/' + shortname; |
| 630 | } |
| 631 | |
| 632 | // Save the user image |
| 633 | if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); } |
| 634 | |
| 635 | // Close the LDAP object |
| 636 | if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } |
| 637 | |
| 638 | // Check if the user already exists |
| 639 | var user = obj.users[userid]; |
| 640 | if (user == null) { |
| 641 | // This user does not exist, create a new account. |
| 642 | 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 }; |
| 643 | if (email) { user['email'] = email; user['emailVerified'] = true; } |
| 644 | if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } |
| 645 | if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; } |
| 646 | var usercount = 0; |
| 647 | for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } } |
| 648 | if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin. |
| 649 | |
| 650 | // Auto-join any user groups |
| 651 | if (typeof domain.newaccountsusergroups == 'object') { |
| 652 | for (var i in domain.newaccountsusergroups) { |
| 653 | var ugrpid = domain.newaccountsusergroups[i]; |
| 654 | if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; } |
| 655 | var ugroup = obj.userGroups[ugrpid]; |
| 656 | if (ugroup != null) { |
| 657 | // Add group to the user |
| 658 | if (user.links == null) { user.links = {}; } |
| 659 | user.links[ugroup._id] = { rights: 1 }; |
| 660 | |
| 661 | // Add user to the group |
| 662 | ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 }; |
| 663 | db.Set(ugroup); |
| 664 | |
| 665 | // Notify user group change |
| 666 | 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 }; |
| 667 | 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. |
| 668 | parent.DispatchEvent(['*', ugroup._id, user._id], obj, event); |
| 669 | } |
| 670 | } |
| 671 | } |
| 672 | |
| 673 | // Check the user real name |
| 674 | if (realname) { user.realname = realname; } |
| 675 | |
| 676 | // Check the user phone number |
| 677 | if (phonenumber) { user.phone = phonenumber; } |
| 678 | |
| 679 | // Indicate that this user has a image |
| 680 | if (userimage != null) { user.flags = 1; } |
| 681 | |
| 682 | // See if the user is a member of the site admin group. |
| 683 | if (typeof siteAdminGroup === 'string') { |
| 684 | parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`); |
| 685 | user.siteadmin = 0xFFFFFFFF; |
| 686 | } |
| 687 | |
| 688 | // Sync the user with LDAP matching user groups |
| 689 | if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; } |
| 690 | |
| 691 | obj.users[user._id] = user; |
| 692 | obj.db.SetUser(user); |
| 693 | 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 }; |
| 694 | 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. |
| 695 | obj.parent.DispatchEvent(['*', 'server-users'], obj, event); |
| 696 | return fn(null, user._id); |
| 697 | } else { |
| 698 | var userChanged = false; |
| 699 | |
| 700 | // This is an existing user |
| 701 | // If the display username has changes, update it. |
| 702 | if (user.name != username) { user.name = username; userChanged = true; } |
| 703 | |
| 704 | // Check if user email has changed |
| 705 | if (user.email && !email) { // email unset in ldap => unset |
| 706 | delete user.email; |
| 707 | delete user.emailVerified; |
| 708 | userChanged = true; |
| 709 | } else if (user.email != email) { // update email |
| 710 | user['email'] = email; |
| 711 | user['emailVerified'] = true; |
| 712 | userChanged = true; |
| 713 | } |
| 714 | |
| 715 | // Check the user real name |
| 716 | if (realname != user.realname) { user.realname = realname; userChanged = true; } |
| 717 | |
| 718 | // Check the user phone number |
| 719 | if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; } |
| 720 | |
| 721 | // Check the user image flag |
| 722 | if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; } |
| 723 | if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; } |
| 724 | |
| 725 | // See if the user is a member of the site admin group. |
| 726 | if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) { |
| 727 | parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`); |
| 728 | user.siteadmin = 0xFFFFFFFF; |
| 729 | userChanged = true; |
| 730 | } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) { |
| 731 | parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`); |
| 732 | delete user.siteadmin; |
| 733 | userChanged = true; |
| 734 | } |
| 735 | |
| 736 | // Synd the user with LDAP matching user groups |
| 737 | if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; } |
| 738 | |
| 739 | // If the user changed, save the changes to the database here |
| 740 | if (userChanged) { |
| 741 | obj.db.SetUser(user); |
| 742 | 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 }; |
| 743 | 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. |
| 744 | parent.DispatchEvent(['*', 'server-users', user._id], obj, event); |
| 745 | } |
| 746 | |
| 747 | // If user is locker out, block here. |
| 748 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } |
| 749 | return fn(null, user._id); |
| 750 | } |
| 751 | } |
| 752 | |
| 753 | if (domain.ldapoptions.url == 'test') { |
| 754 | // Test LDAP login |
| 755 | var xxuser = domain.ldapoptions[name.toLowerCase()]; |
| 756 | if (xxuser == null) { fn(new Error('invalid password')); return; } else { |
| 757 | ldapHandler.ldapShortName = name.toLowerCase(); |
| 758 | if (typeof xxuser == 'string') { |
| 759 | // The test LDAP user points to a JSON file where the user information is, load it. |
| 760 | ldapHandler(null, require(xxuser)); |
| 761 | } else { |
| 762 | // The test user information is in the config.json, use it. |
| 763 | ldapHandler(null, xxuser); |
| 764 | } |
| 765 | } |
| 766 | } else { |
| 767 | // LDAP login |
| 768 | var LdapAuth = require('ldapauth-fork'); |
| 769 | if (domain.ldapoptions == null) { domain.ldapoptions = {}; } |
| 770 | domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images. |
| 771 | var ldap = new LdapAuth(domain.ldapoptions); |
| 772 | ldapHandler.ldapobj = ldap; |
| 773 | ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object |
| 774 | ldap.authenticate(name, pass, ldapHandler); |
| 775 | } |
| 776 | } else { |
| 777 | // Regular login |
| 778 | var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()]; |
| 779 | // Query the db for the given username |
| 780 | if (!user) { fn(new Error('cannot find user')); return; } |
| 781 | // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user |
| 782 | if (user.salt == null) { |
| 783 | fn(new Error('invalid password')); |
| 784 | } else { |
| 785 | if (user.passtype != null) { |
| 786 | // IIS default clear or weak password hashing (SHA-1) |
| 787 | require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) { |
| 788 | if (err) return fn(err); |
| 789 | if (hash == user.hash) { |
| 790 | // Update the password to the stronger format. |
| 791 | 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); |
| 792 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } |
| 793 | return fn(null, user._id); |
| 794 | } |
| 795 | fn(new Error('invalid password'), null, user.passhint); |
| 796 | }); |
| 797 | } else { |
| 798 | // Default strong password hashing (pbkdf2 SHA384) |
| 799 | require('./pass').hash(pass, user.salt, function (err, hash, tag) { |
| 800 | if (err) return fn(err); |
| 801 | if (hash == user.hash) { |
| 802 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } |
| 803 | return fn(null, user._id); |
| 804 | } |
| 805 | fn(new Error('invalid password'), null, user.passhint); |
| 806 | }, 0); |
| 807 | } |
| 808 | } |
| 809 | } |
| 810 | }; |
| 811 | |
| 812 | /* |
| 813 | obj.restrict = function (req, res, next) { |
| 814 | console.log('restrict', req.url); |
| 815 | var domain = getDomain(req); |
| 816 | if (req.session.userid) { |
| 817 | next(); |
| 818 | } else { |
| 819 | req.session.messageid = 111; // Access denied. |
| 820 | res.redirect(domain.url + 'login'); |
| 821 | } |
| 822 | }; |
| 823 | */ |
| 824 | |
| 825 | // Check if the source IP address is in the IP list, return false if not. |
| 826 | function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) { |
| 827 | try { |
| 828 | if (req.connection) { |
| 829 | // HTTP(S) request |
| 830 | 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; } } } |
| 831 | if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } } |
| 832 | } else { |
| 833 | // WebSocket request |
| 834 | 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; } } } |
| 835 | if (closeIfThis === false) { try { req.close(); } catch (e) { } } |
| 836 | } |
| 837 | } catch (e) { console.log(e); } // Should never happen |
| 838 | return false; |
| 839 | } |
| 840 | |
| 841 | // Check if the source IP address is allowed, return domain if allowed |
| 842 | // If there is a fail and null is returned, the request or connection is closed already. |
| 843 | function checkUserIpAddress(req, res) { |
| 844 | if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; } |
| 845 | if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; } |
| 846 | const domain = (req.url ? getDomain(req) : getDomain(res)); |
| 847 | if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; } |
| 848 | if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; } |
| 849 | if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; } |
| 850 | return domain; |
| 851 | } |
| 852 | |
| 853 | // Check if the source IP address is allowed, return domain if allowed |
| 854 | // If there is a fail and null is returned, the request or connection is closed already. |
| 855 | function checkAgentIpAddress(req, res) { |
| 856 | if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; } |
| 857 | if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; } |
| 858 | const domain = (req.url ? getDomain(req) : getDomain(res)); |
| 859 | if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; } |
| 860 | if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; } |
| 861 | return domain; |
| 862 | } |
| 863 | |
| 864 | // Return the current domain of the request |
| 865 | // Request or connection says open regardless of the response |
| 866 | function getDomain(req) { |
| 867 | if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it. |
| 868 | 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 |
| 869 | 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. |
| 870 | const x = req.url.split('/'); |
| 871 | if (x.length < 2) return parent.config.domains['']; |
| 872 | const y = parent.config.domains[x[1].toLowerCase()]; |
| 873 | if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; } |
| 874 | return parent.config.domains['']; |
| 875 | } |
| 876 | |
| 877 | function parseAllowedFramingOrigins(val) { |
| 878 | if (val == null) return []; |
| 879 | var arr = []; |
| 880 | if (Array.isArray(val)) { arr = val.slice(); } else if (typeof val == 'string') { arr = val.split(',').map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; }); } else { return []; } |
| 881 | var out = []; |
| 882 | for (var i = 0; i < arr.length; i++) { |
| 883 | var o = arr[i].trim().replace(/\/+$/, ''); |
| 884 | if (o.length === 0) continue; |
| 885 | if (o.indexOf('https://') === 0 || o.indexOf('http://') === 0) { out.push(o); } |
| 886 | } |
| 887 | return out; |
| 888 | } |
| 889 | |
| 890 | function handleLogoutRequest(req, res) { |
| 891 | const domain = checkUserIpAddress(req, res); |
| 892 | if (domain == null) { return; } |
| 893 | if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; } |
| 894 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 895 | |
| 896 | // If a HTTP header is required, check new UserRequiredHttpHeader |
| 897 | 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; } } |
| 898 | |
| 899 | res.set({ 'Cache-Control': 'no-store' }); |
| 900 | // Destroy the user's session to log them out will be re-created next request |
| 901 | var userid = req.session.userid; |
| 902 | if (req.session.userid) { |
| 903 | var user = obj.users[req.session.userid]; |
| 904 | if (user != null) { |
| 905 | obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] }); |
| 906 | obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id }); |
| 907 | } |
| 908 | if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session |
| 909 | } |
| 910 | req.session = null; |
| 911 | parent.debug('web', 'handleLogoutRequest: success.'); |
| 912 | |
| 913 | // If this user was logged in using an authentication strategy and there is a logout URL, use it. |
| 914 | if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) { |
| 915 | let logouturl = null; |
| 916 | let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1); |
| 917 | // Setup logout url for oidc |
| 918 | if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) { |
| 919 | if (typeof domain.authstrategies.oidc.logouturl == 'string') { |
| 920 | logouturl = domain.authstrategies.oidc.logouturl; |
| 921 | } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') { |
| 922 | logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + (domain.authstrategies.oidc.issuer.end_session_endpoint.indexOf('?') == -1 ? '?' : '&') + 'post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri; |
| 923 | } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') { |
| 924 | logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint; |
| 925 | } |
| 926 | // Log out all other strategies |
| 927 | } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; } |
| 928 | // If custom logout was setup, use it |
| 929 | if (logouturl != null) { |
| 930 | parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl); |
| 931 | res.redirect(logouturl); |
| 932 | return; |
| 933 | } |
| 934 | } |
| 935 | |
| 936 | // This is the default logout redirect to the login page |
| 937 | if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); } |
| 938 | } |
| 939 | |
| 940 | // Return an object with 2FA type if 2-step auth can be skipped |
| 941 | function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) { |
| 942 | if (parent.config.settings.no2factorauth == true) return null; |
| 943 | |
| 944 | // If this login occurred using a login token, no 2FA needed. |
| 945 | if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; } |
| 946 | |
| 947 | // Check if we can skip 2nd factor auth because of the source IP address |
| 948 | if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) { |
| 949 | for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } } |
| 950 | } |
| 951 | |
| 952 | // Check if a 2nd factor cookie is present |
| 953 | if (typeof req.headers.cookie == 'string') { |
| 954 | const cookies = req.headers.cookie.split('; '); |
| 955 | for (var i in cookies) { |
| 956 | if (cookies[i].startsWith('twofactor=')) { |
| 957 | 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. |
| 958 | if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; } |
| 959 | } |
| 960 | } |
| 961 | } |
| 962 | |
| 963 | return null; |
| 964 | } |
| 965 | |
| 966 | // Return true if this user has 2-step auth active |
| 967 | function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) { |
| 968 | // If this login occurred using a login token, no 2FA needed. |
| 969 | if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; } |
| 970 | |
| 971 | // Check if we can skip 2nd factor auth because of the source IP address |
| 972 | if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) { |
| 973 | for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; } |
| 974 | } |
| 975 | |
| 976 | // Check if a 2nd factor cookie is present |
| 977 | if (typeof req.headers.cookie == 'string') { |
| 978 | const cookies = req.headers.cookie.split('; '); |
| 979 | for (var i in cookies) { |
| 980 | if (cookies[i].startsWith('twofactor=')) { |
| 981 | 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. |
| 982 | if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; } |
| 983 | } |
| 984 | } |
| 985 | } |
| 986 | |
| 987 | // See if SMS 2FA is available |
| 988 | var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); |
| 989 | |
| 990 | // See if Messenger 2FA is available |
| 991 | var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null)); |
| 992 | |
| 993 | // Check if a 2nd factor is present |
| 994 | return ((parent.config.settings.no2factorauth !== true) && (msg2fa || sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || (user.otpduo != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0)))); |
| 995 | } |
| 996 | |
| 997 | // Check the 2-step auth token |
| 998 | function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) { |
| 999 | parent.debug('web', 'checkUserOneTimePassword()'); |
| 1000 | const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true)); |
| 1001 | if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; }; |
| 1002 | |
| 1003 | // Check if we can use OTP tokens with email |
| 1004 | var otpemail = (domain.mailserver != null); |
| 1005 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } |
| 1006 | var otpsms = (parent.smsserver != null); |
| 1007 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } |
| 1008 | var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0)); |
| 1009 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; } |
| 1010 | |
| 1011 | // Check 2FA login cookie |
| 1012 | if ((token != null) && (token.startsWith('cookie='))) { |
| 1013 | 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. |
| 1014 | if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; } |
| 1015 | } |
| 1016 | |
| 1017 | // Check email key |
| 1018 | if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) { |
| 1019 | var deltaTime = (Date.now() - user.otpekey.d); |
| 1020 | if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5). |
| 1021 | user.otpekey = {}; |
| 1022 | obj.db.SetUser(user); |
| 1023 | parent.debug('web', 'checkUserOneTimePassword: success (email).'); |
| 1024 | func(true, { twoFactorType: 'email' }); |
| 1025 | return; |
| 1026 | } |
| 1027 | } |
| 1028 | |
| 1029 | // Check SMS key |
| 1030 | if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) { |
| 1031 | var deltaTime = (Date.now() - user.otpsms.d); |
| 1032 | if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5). |
| 1033 | delete user.otpsms; |
| 1034 | obj.db.SetUser(user); |
| 1035 | parent.debug('web', 'checkUserOneTimePassword: success (SMS).'); |
| 1036 | func(true, { twoFactorType: 'sms' }); |
| 1037 | return; |
| 1038 | } |
| 1039 | } |
| 1040 | |
| 1041 | // Check messenger key |
| 1042 | if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) { |
| 1043 | var deltaTime = (Date.now() - user.otpmsg.d); |
| 1044 | if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5). |
| 1045 | delete user.otpmsg; |
| 1046 | obj.db.SetUser(user); |
| 1047 | parent.debug('web', 'checkUserOneTimePassword: success (Messenger).'); |
| 1048 | func(true, { twoFactorType: 'messenger' }); |
| 1049 | return; |
| 1050 | } |
| 1051 | } |
| 1052 | |
| 1053 | // Check hardware key |
| 1054 | if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) { |
| 1055 | var authResponse = null; |
| 1056 | try { authResponse = JSON.parse(hwtoken); } catch (ex) { } |
| 1057 | if ((authResponse != null) && (authResponse.clientDataJSON)) { |
| 1058 | // Get all WebAuthn keys |
| 1059 | var webAuthnKeys = []; |
| 1060 | for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } } |
| 1061 | if (webAuthnKeys.length > 0) { |
| 1062 | // Decode authentication response |
| 1063 | var clientAssertionResponse = { response: {} }; |
| 1064 | clientAssertionResponse.id = authResponse.id; |
| 1065 | clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64'); |
| 1066 | clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64'); |
| 1067 | clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64'); |
| 1068 | clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64'); |
| 1069 | clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64'); |
| 1070 | |
| 1071 | // Look for the key with clientAssertionResponse.id |
| 1072 | var webAuthnKey = null; |
| 1073 | for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } } |
| 1074 | |
| 1075 | // If we found a valid key to use, let's validate the response |
| 1076 | if (webAuthnKey != null) { |
| 1077 | // Figure out the origin |
| 1078 | var httpport = ((args.aliasport != null) ? args.aliasport : args.port); |
| 1079 | var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName); |
| 1080 | if (httpport != 443) { origin += ':' + httpport; } |
| 1081 | |
| 1082 | var u2fchallenge = null; |
| 1083 | if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } } |
| 1084 | var assertionExpectations = { |
| 1085 | challenge: u2fchallenge, |
| 1086 | origin: origin, |
| 1087 | factor: 'either', |
| 1088 | fmt: 'fido-u2f', |
| 1089 | publicKey: webAuthnKey.publicKey, |
| 1090 | prevCounter: webAuthnKey.counter, |
| 1091 | userHandle: Buffer.from(user._id, 'binary').toString('base64') |
| 1092 | }; |
| 1093 | |
| 1094 | var webauthnResponse = null; |
| 1095 | try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); } |
| 1096 | if ((webauthnResponse != null) && (webauthnResponse.verified === true)) { |
| 1097 | // Update the hardware key counter and accept the 2nd factor |
| 1098 | webAuthnKey.counter = webauthnResponse.counter; |
| 1099 | obj.db.SetUser(user); |
| 1100 | parent.debug('web', 'checkUserOneTimePassword: success (hardware).'); |
| 1101 | func(true, { twoFactorType: 'fido' }); |
| 1102 | } else { |
| 1103 | parent.debug('web', 'checkUserOneTimePassword: fail (hardware).'); |
| 1104 | func(false); |
| 1105 | } |
| 1106 | return; |
| 1107 | } |
| 1108 | } |
| 1109 | } |
| 1110 | } |
| 1111 | |
| 1112 | // Check Google Authenticator |
| 1113 | if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6)){ |
| 1114 | const otplib = require('otplib'); |
| 1115 | const verified = otplib.verifySync({ |
| 1116 | epochTolerance: 60, |
| 1117 | token: token, |
| 1118 | secret: user.otpsecret, |
| 1119 | guardrails: otplib.createGuardrails({ |
| 1120 | MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105 |
| 1121 | }) |
| 1122 | }); |
| 1123 | if (verified.valid === true) { |
| 1124 | parent.debug('web', 'checkUserOneTimePassword: success (authenticator).'); |
| 1125 | func(true, { twoFactorType: 'otp' }); |
| 1126 | return; |
| 1127 | } |
| 1128 | }; |
| 1129 | |
| 1130 | // Check written down keys |
| 1131 | if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) { |
| 1132 | var tokenNumber = parseInt(token); |
| 1133 | for (var i = 0; i < user.otpkeys.keys.length; i++) { |
| 1134 | if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { |
| 1135 | parent.debug('web', 'checkUserOneTimePassword: success (one-time).'); |
| 1136 | user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return; |
| 1137 | } |
| 1138 | } |
| 1139 | } |
| 1140 | |
| 1141 | // Check OTP hardware key (Yubikey OTP) |
| 1142 | 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)) { |
| 1143 | var keyId = token.substring(0, 12); |
| 1144 | |
| 1145 | // Find a matching OTP key |
| 1146 | var match = false; |
| 1147 | for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } } |
| 1148 | |
| 1149 | // If we have a match, check the OTP |
| 1150 | if (match === true) { |
| 1151 | var yub = require('yub'); |
| 1152 | yub.init(domain.yubikey.id, domain.yubikey.secret); |
| 1153 | yub.verify(token, function (err, results) { |
| 1154 | if ((results != null) && (results.status == 'OK')) { |
| 1155 | parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).'); |
| 1156 | func(true, { twoFactorType: 'hwotp' }); |
| 1157 | } else { |
| 1158 | parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).'); |
| 1159 | func(false); |
| 1160 | } |
| 1161 | }); |
| 1162 | return; |
| 1163 | } |
| 1164 | } |
| 1165 | |
| 1166 | parent.debug('web', 'checkUserOneTimePassword: fail (2).'); |
| 1167 | func(false); |
| 1168 | } |
| 1169 | |
| 1170 | // Return a U2F hardware key challenge |
| 1171 | function getHardwareKeyChallenge(req, domain, user, func) { |
| 1172 | var sec = {}; |
| 1173 | if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } } |
| 1174 | |
| 1175 | if (user.otphkeys && (user.otphkeys.length > 0)) { |
| 1176 | // Get all WebAuthn keys |
| 1177 | var webAuthnKeys = []; |
| 1178 | for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } } |
| 1179 | if (webAuthnKeys.length > 0) { |
| 1180 | // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this. |
| 1181 | var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') }; |
| 1182 | // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin. |
| 1183 | authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{ |
| 1184 | for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); } |
| 1185 | sec.u2f = authnOptions.challenge; |
| 1186 | req.session.e = parent.encryptSessionData(sec); |
| 1187 | parent.debug('web', 'getHardwareKeyChallenge: success'); |
| 1188 | func(JSON.stringify(authnOptions)); |
| 1189 | return; |
| 1190 | } |
| 1191 | } |
| 1192 | |
| 1193 | // Remove the challenge if present |
| 1194 | if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); } |
| 1195 | |
| 1196 | parent.debug('web', 'getHardwareKeyChallenge: fail'); |
| 1197 | func(''); |
| 1198 | } |
| 1199 | |
| 1200 | // Redirect a root request to a different page |
| 1201 | function handleRootRedirect(req, res, direct) { |
| 1202 | const domain = checkUserIpAddress(req, res); |
| 1203 | if (domain == null) { return; } |
| 1204 | res.redirect(domain.rootredirect + getQueryPortion(req)); |
| 1205 | } |
| 1206 | |
| 1207 | function handleLoginRequest(req, res, direct) { |
| 1208 | const domain = checkUserIpAddress(req, res); |
| 1209 | if (domain == null) { return; } |
| 1210 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 1211 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 1212 | if (req.session == null) { req.session = {}; } |
| 1213 | |
| 1214 | // Check if this is a banned ip address |
| 1215 | if (obj.checkAllowLogin(req) == false) { |
| 1216 | // Wait and redirect the user |
| 1217 | setTimeout(function () { |
| 1218 | req.session.messageid = 114; // IP address blocked, try again later. |
| 1219 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1220 | }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); |
| 1221 | return; |
| 1222 | } |
| 1223 | |
| 1224 | // Normally, use the body username/password. If this is a token, use the username/password in the session. |
| 1225 | var xusername = req.body.username, xpassword = req.body.password; |
| 1226 | if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { |
| 1227 | const sec = parent.decryptSessionData(req.session.e); |
| 1228 | xusername = sec.tuser; xpassword = sec.tpass; |
| 1229 | } |
| 1230 | |
| 1231 | // Authenticate the user |
| 1232 | obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) { |
| 1233 | if (userid) { |
| 1234 | var user = obj.users[userid]; |
| 1235 | |
| 1236 | // Check if we are in maintenance mode |
| 1237 | if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) { |
| 1238 | req.session.messageid = 115; // Server under maintenance |
| 1239 | req.session.loginmode = 1; |
| 1240 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1241 | return; |
| 1242 | } |
| 1243 | |
| 1244 | var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null)); |
| 1245 | var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); |
| 1246 | var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null)); |
| 1247 | var push2fa = ((parent.firebase != null) && (user.otpdev != null)); |
| 1248 | var duo2fa = ((((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) || ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor != false))) && (user.otpduo != null)); |
| 1249 | |
| 1250 | // Check if two factor can be skipped |
| 1251 | const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions); |
| 1252 | |
| 1253 | // Check if this user has 2-step login active |
| 1254 | if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) { |
| 1255 | if ((req.body.hwtoken == '**timeout**')) { |
| 1256 | delete req.session; // Clear the session |
| 1257 | res.redirect(domain.url + getQueryPortion(req)); |
| 1258 | return; |
| 1259 | } |
| 1260 | |
| 1261 | if ((req.body.hwtoken == '**email**') && email2fa) { |
| 1262 | user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; |
| 1263 | obj.db.SetUser(user); |
| 1264 | parent.debug('web', 'Sending 2FA email to: ' + user.email); |
| 1265 | domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); |
| 1266 | req.session.messageid = 2; // "Email sent" message |
| 1267 | req.session.loginmode = 4; |
| 1268 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1269 | return; |
| 1270 | } |
| 1271 | |
| 1272 | if ((req.body.hwtoken == '**sms**') && sms2fa) { |
| 1273 | // Cause a token to be sent to the user's phone number |
| 1274 | user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() }; |
| 1275 | obj.db.SetUser(user); |
| 1276 | parent.debug('web', 'Sending 2FA SMS to: ' + user.phone); |
| 1277 | parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req)); |
| 1278 | // Ask for a login token & confirm sms was sent |
| 1279 | req.session.messageid = 4; // "SMS sent" message |
| 1280 | req.session.loginmode = 4; |
| 1281 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1282 | return; |
| 1283 | } |
| 1284 | |
| 1285 | if ((req.body.hwtoken == '**msg**') && msg2fa) { |
| 1286 | // Cause a token to be sent to the user's messenger account |
| 1287 | user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() }; |
| 1288 | obj.db.SetUser(user); |
| 1289 | parent.debug('web', 'Sending 2FA message to: ' + user.msghandle); |
| 1290 | parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req)); |
| 1291 | // Ask for a login token & confirm message was sent |
| 1292 | req.session.messageid = 6; // "Message sent" message |
| 1293 | req.session.loginmode = 4; |
| 1294 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1295 | return; |
| 1296 | } |
| 1297 | |
| 1298 | if ((req.body.hwtoken == '**duo**') && duo2fa && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { |
| 1299 | // Redirect to duo here |
| 1300 | const duo = require('@duosecurity/duo_universal'); |
| 1301 | const client = new duo.Client({ |
| 1302 | clientId: domain.duo2factor.integrationkey, |
| 1303 | clientSecret: domain.duo2factor.secretkey, |
| 1304 | apiHost: domain.duo2factor.apihostname, |
| 1305 | redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '') |
| 1306 | }); |
| 1307 | // Decrypt any session data |
| 1308 | const sec = parent.decryptSessionData(req.session.e); |
| 1309 | sec.duostate = client.generateState(); |
| 1310 | req.session.e = parent.encryptSessionData(sec); |
| 1311 | parent.debug('web', 'Redirecting user ' + user._id + ' to Duo'); |
| 1312 | res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate)); |
| 1313 | return; |
| 1314 | } |
| 1315 | |
| 1316 | // Handle device push notification 2FA request |
| 1317 | // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification. |
| 1318 | if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) { |
| 1319 | const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64'); |
| 1320 | const sessioncode = obj.crypto.randomBytes(24).toString('base64'); |
| 1321 | |
| 1322 | // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject. |
| 1323 | const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id }); |
| 1324 | |
| 1325 | // Get the HTTPS port |
| 1326 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified |
| 1327 | |
| 1328 | // Get the agent connection server name |
| 1329 | var serverName = obj.getWebServerName(domain, req); |
| 1330 | if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; } |
| 1331 | |
| 1332 | // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly. |
| 1333 | var xdomain = (domain.dns == null) ? domain.id : ''; |
| 1334 | if (xdomain != '') xdomain += '/'; |
| 1335 | var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie; |
| 1336 | |
| 1337 | // Request that the login page wait for device auth |
| 1338 | req.session.messageid = 5; // "Sending notification..." message |
| 1339 | req.session.passhint = url; |
| 1340 | req.session.loginmode = 8; |
| 1341 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1342 | return; |
| 1343 | } |
| 1344 | |
| 1345 | checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) { |
| 1346 | if (result == false) { |
| 1347 | var randomWaitTime = 0; |
| 1348 | |
| 1349 | // Check if 2FA is allowed for this IP address |
| 1350 | if (obj.checkAllow2Fa(req) == false) { |
| 1351 | // Wait and redirect the user |
| 1352 | setTimeout(function () { |
| 1353 | req.session.messageid = 114; // IP address blocked, try again later. |
| 1354 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1355 | }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); |
| 1356 | return; |
| 1357 | } |
| 1358 | |
| 1359 | // 2-step auth is required, but the token is not present or not valid. |
| 1360 | if ((req.body.token != null) || (req.body.hwtoken != null)) { |
| 1361 | randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds. |
| 1362 | req.session.messageid = 108; // Invalid token, try again. |
| 1363 | obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] }); |
| 1364 | parent.debug('web', 'handleLoginRequest: invalid 2FA token'); |
| 1365 | const ua = obj.getUserAgentInfo(req); |
| 1366 | 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] }); |
| 1367 | obj.setbad2Fa(req); |
| 1368 | } else { |
| 1369 | parent.debug('web', 'handleLoginRequest: 2FA token required'); |
| 1370 | } |
| 1371 | |
| 1372 | // Wait and redirect the user |
| 1373 | setTimeout(function () { |
| 1374 | req.session.loginmode = 4; |
| 1375 | if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; } |
| 1376 | if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; } |
| 1377 | if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; } |
| 1378 | if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; } |
| 1379 | if ((user.otpduo != null)) { req.session.tduo = 1; } |
| 1380 | req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword }); |
| 1381 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1382 | }, randomWaitTime); |
| 1383 | } else { |
| 1384 | // Check if we need to remember this device |
| 1385 | if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) { |
| 1386 | var maxCookieAge = domain.twofactorcookiedurationdays; |
| 1387 | if (typeof maxCookieAge != 'number') { maxCookieAge = 30; } |
| 1388 | const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey); |
| 1389 | res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true }); |
| 1390 | } |
| 1391 | |
| 1392 | // Check if email address needs to be confirmed |
| 1393 | 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')) |
| 1394 | if (emailcheck && (user.emailVerified !== true)) { |
| 1395 | parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page'); |
| 1396 | req.session.messageid = 3; // "Email verification required" message |
| 1397 | req.session.loginmode = 7; |
| 1398 | req.session.passhint = user.email; |
| 1399 | req.session.cuserid = userid; |
| 1400 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1401 | return; |
| 1402 | } |
| 1403 | |
| 1404 | // Login successful |
| 1405 | parent.debug('web', 'handleLoginRequest: successful 2FA login'); |
| 1406 | if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; } |
| 1407 | completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions); |
| 1408 | } |
| 1409 | }); |
| 1410 | return; |
| 1411 | } |
| 1412 | |
| 1413 | // Check if email address needs to be confirmed |
| 1414 | 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')) |
| 1415 | if (emailcheck && (user.emailVerified !== true)) { |
| 1416 | parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page'); |
| 1417 | req.session.messageid = 3; // "Email verification required" message |
| 1418 | req.session.loginmode = 7; |
| 1419 | req.session.passhint = user.email; |
| 1420 | req.session.cuserid = userid; |
| 1421 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1422 | return; |
| 1423 | } |
| 1424 | |
| 1425 | // Login successful |
| 1426 | parent.debug('web', 'handleLoginRequest: successful login'); |
| 1427 | if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; } |
| 1428 | completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions); |
| 1429 | } else { |
| 1430 | // Login failed, log the error |
| 1431 | obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] }); |
| 1432 | |
| 1433 | // Wait a random delay |
| 1434 | setTimeout(function () { |
| 1435 | // If the account is locked, display that. |
| 1436 | if (typeof xusername == 'string') { |
| 1437 | var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase(); |
| 1438 | if (err == 'locked') { |
| 1439 | parent.debug('web', 'handleLoginRequest: login failed, locked account'); |
| 1440 | req.session.messageid = 110; // Account locked. |
| 1441 | const ua = obj.getUserAgentInfo(req); |
| 1442 | 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] }); |
| 1443 | obj.setbadLogin(req); |
| 1444 | } else if (err == 'denied') { |
| 1445 | parent.debug('web', 'handleLoginRequest: login failed, access denied'); |
| 1446 | req.session.messageid = 111; // Access denied. |
| 1447 | const ua = obj.getUserAgentInfo(req); |
| 1448 | 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] }); |
| 1449 | obj.setbadLogin(req); |
| 1450 | } else { |
| 1451 | parent.debug('web', 'handleLoginRequest: login failed, bad username and password'); |
| 1452 | req.session.messageid = 112; // Login failed, check username and password. |
| 1453 | const ua = obj.getUserAgentInfo(req); |
| 1454 | 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] }); |
| 1455 | obj.setbadLogin(req); |
| 1456 | } |
| 1457 | } |
| 1458 | |
| 1459 | // Clean up login mode and display password hint if present. |
| 1460 | delete req.session.loginmode; |
| 1461 | if ((passhint != null) && (passhint.length > 0)) { |
| 1462 | req.session.passhint = passhint; |
| 1463 | } else { |
| 1464 | delete req.session.passhint; |
| 1465 | } |
| 1466 | |
| 1467 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1468 | }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds. |
| 1469 | } |
| 1470 | }); |
| 1471 | } |
| 1472 | |
| 1473 | function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) { |
| 1474 | // Check if we need to change the password |
| 1475 | 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))))) { |
| 1476 | // Request a password change |
| 1477 | parent.debug('web', 'handleLoginRequest: login ok, password change requested'); |
| 1478 | req.session.loginmode = 6; |
| 1479 | req.session.messageid = 113; // Password change requested. |
| 1480 | |
| 1481 | // Decrypt any session data |
| 1482 | const sec = parent.decryptSessionData(req.session.e); |
| 1483 | sec.rtuser = xusername; |
| 1484 | sec.rtpass = xpassword; |
| 1485 | sec.rtreset = true; |
| 1486 | req.session.e = parent.encryptSessionData(sec); |
| 1487 | |
| 1488 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1489 | return; |
| 1490 | } |
| 1491 | |
| 1492 | // Save login time |
| 1493 | user.pastlogin = user.login; |
| 1494 | user.login = user.access = Math.floor(Date.now() / 1000); |
| 1495 | obj.db.SetUser(user); |
| 1496 | |
| 1497 | // Notify account login |
| 1498 | const targets = ['*', 'server-users', user._id]; |
| 1499 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 1500 | const ua = obj.getUserAgentInfo(req); |
| 1501 | 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 }; |
| 1502 | if (loginOptions != null) { |
| 1503 | 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. |
| 1504 | if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; } |
| 1505 | } |
| 1506 | obj.parent.DispatchEvent(targets, obj, loginEvent); |
| 1507 | |
| 1508 | // Regenerate session when signing in to prevent fixation |
| 1509 | //req.session.regenerate(function () { |
| 1510 | // Store the user's primary key in the session store to be retrieved, or in this case the entire user object |
| 1511 | delete req.session.e; |
| 1512 | delete req.session.u2f; |
| 1513 | delete req.session.loginmode; |
| 1514 | delete req.session.tuserid; |
| 1515 | delete req.session.tuser; |
| 1516 | delete req.session.tpass; |
| 1517 | delete req.session.temail; |
| 1518 | delete req.session.tsms; |
| 1519 | delete req.session.tmsg; |
| 1520 | delete req.session.tduo; |
| 1521 | delete req.session.tpush; |
| 1522 | delete req.session.messageid; |
| 1523 | delete req.session.passhint; |
| 1524 | delete req.session.cuserid; |
| 1525 | delete req.session.expire; |
| 1526 | delete req.session.currentNode; |
| 1527 | req.session.userid = userid; |
| 1528 | req.session.ip = req.clientIp; |
| 1529 | setSessionRandom(req); |
| 1530 | 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 }); |
| 1531 | |
| 1532 | // If a login token was used, add this information and expire time to the session. |
| 1533 | if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { |
| 1534 | req.session.loginToken = loginOptions.tokenUser; |
| 1535 | if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; } |
| 1536 | } |
| 1537 | |
| 1538 | if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; } |
| 1539 | if (req.body.host) { |
| 1540 | // TODO: This is a terrible search!!! FIX THIS. |
| 1541 | /* |
| 1542 | obj.db.GetAllType('node', function (err, docs) { |
| 1543 | for (var i = 0; i < docs.length; i++) { |
| 1544 | if (docs[i].name == req.body.host) { |
| 1545 | req.session.currentNode = docs[i]._id; |
| 1546 | break; |
| 1547 | } |
| 1548 | } |
| 1549 | console.log("CurrentNode: " + req.session.currentNode); |
| 1550 | // This redirect happens after finding node is completed |
| 1551 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1552 | }); |
| 1553 | */ |
| 1554 | parent.debug('web', 'handleLoginRequest: login ok (1)'); |
| 1555 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary |
| 1556 | } else { |
| 1557 | parent.debug('web', 'handleLoginRequest: login ok (2)'); |
| 1558 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1559 | } |
| 1560 | //}); |
| 1561 | } |
| 1562 | |
| 1563 | function handleCreateAccountRequest(req, res, direct) { |
| 1564 | const domain = checkUserIpAddress(req, res); |
| 1565 | if (domain == null) { return; } |
| 1566 | if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; } |
| 1567 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 1568 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 1569 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 1570 | |
| 1571 | // Check if we are in maintenance mode |
| 1572 | if (parent.config.settings.maintenancemode != null) { |
| 1573 | req.session.messageid = 115; // Server under maintenance |
| 1574 | req.session.loginmode = 1; |
| 1575 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1576 | return; |
| 1577 | } |
| 1578 | |
| 1579 | // Always lowercase the email address |
| 1580 | if (req.body.email) { req.body.email = req.body.email.toLowerCase(); } |
| 1581 | |
| 1582 | // If the email is the username, set this here. |
| 1583 | if (domain.usernameisemail) { req.body.username = req.body.email; } |
| 1584 | |
| 1585 | // Check if there is domain.newAccountToken, check if supplied token is valid |
| 1586 | if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) { |
| 1587 | parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token'); |
| 1588 | req.session.loginmode = 2; |
| 1589 | req.session.messageid = 103; // Invalid account creation token. |
| 1590 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1591 | return; |
| 1592 | } |
| 1593 | |
| 1594 | // If needed, check the new account creation CAPTCHA |
| 1595 | if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) { |
| 1596 | const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout |
| 1597 | if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) { |
| 1598 | req.session.loginmode = 2; |
| 1599 | req.session.messageid = 117; // Invalid security check |
| 1600 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1601 | return; |
| 1602 | } |
| 1603 | } |
| 1604 | |
| 1605 | // Accounts that start with ~ are not allowed |
| 1606 | if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) { |
| 1607 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)'); |
| 1608 | req.session.loginmode = 2; |
| 1609 | req.session.messageid = 100; // Unable to create account. |
| 1610 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1611 | return; |
| 1612 | } |
| 1613 | |
| 1614 | // Count the number of users in this domain |
| 1615 | var domainUserCount = 0; |
| 1616 | for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } } |
| 1617 | |
| 1618 | // Check if we are allowed to create new users using the login screen |
| 1619 | if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) { |
| 1620 | parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.'); |
| 1621 | res.sendStatus(401); |
| 1622 | return; |
| 1623 | } |
| 1624 | |
| 1625 | // Check if this request is for an allows email domain |
| 1626 | if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) { |
| 1627 | var i = -1; |
| 1628 | if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); } |
| 1629 | if (i == -1) { |
| 1630 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)'); |
| 1631 | req.session.loginmode = 2; |
| 1632 | req.session.messageid = 100; // Unable to create account. |
| 1633 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1634 | return; |
| 1635 | } |
| 1636 | var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase(); |
| 1637 | for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } } |
| 1638 | if (emailok == false) { |
| 1639 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)'); |
| 1640 | req.session.loginmode = 2; |
| 1641 | req.session.messageid = 100; // Unable to create account. |
| 1642 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1643 | return; |
| 1644 | } |
| 1645 | } |
| 1646 | |
| 1647 | // Check if we exceed the maximum number of user accounts |
| 1648 | obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) { |
| 1649 | if (maxExceed) { |
| 1650 | parent.debug('web', 'handleCreateAccountRequest: account limit reached'); |
| 1651 | req.session.loginmode = 2; |
| 1652 | req.session.messageid = 101; // Account limit reached. |
| 1653 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1654 | } else { |
| 1655 | 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)) { |
| 1656 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)'); |
| 1657 | req.session.loginmode = 2; |
| 1658 | req.session.messageid = 100; // Unable to create account. |
| 1659 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1660 | } else { |
| 1661 | // Check if this email was already verified |
| 1662 | obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) { |
| 1663 | if ((docs != null) && (docs.length > 0)) { |
| 1664 | parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address'); |
| 1665 | req.session.loginmode = 2; |
| 1666 | req.session.messageid = 102; // Existing account with this email address. |
| 1667 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1668 | } else { |
| 1669 | // Check if user exists |
| 1670 | if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) { |
| 1671 | parent.debug('web', 'handleCreateAccountRequest: Username already exists'); |
| 1672 | req.session.loginmode = 2; |
| 1673 | req.session.messageid = 104; // Username already exists. |
| 1674 | } else { |
| 1675 | 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 }; |
| 1676 | if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } |
| 1677 | if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; } |
| 1678 | 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; } |
| 1679 | if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin. |
| 1680 | |
| 1681 | // Auto-join any user groups |
| 1682 | if (typeof domain.newaccountsusergroups == 'object') { |
| 1683 | for (var i in domain.newaccountsusergroups) { |
| 1684 | var ugrpid = domain.newaccountsusergroups[i]; |
| 1685 | if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; } |
| 1686 | var ugroup = obj.userGroups[ugrpid]; |
| 1687 | if (ugroup != null) { |
| 1688 | // Add group to the user |
| 1689 | if (user.links == null) { user.links = {}; } |
| 1690 | user.links[ugroup._id] = { rights: 1 }; |
| 1691 | |
| 1692 | // Add user to the group |
| 1693 | ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 }; |
| 1694 | db.Set(ugroup); |
| 1695 | |
| 1696 | // Notify user group change |
| 1697 | 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 }; |
| 1698 | 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. |
| 1699 | parent.DispatchEvent(['*', ugroup._id, user._id], obj, event); |
| 1700 | } |
| 1701 | } |
| 1702 | } |
| 1703 | |
| 1704 | obj.users[user._id] = user; |
| 1705 | req.session.userid = user._id; |
| 1706 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 1707 | setSessionRandom(req); |
| 1708 | // Create a user, generate a salt and hash the password |
| 1709 | require('./pass').hash(req.body.password1, function (err, salt, hash, tag) { |
| 1710 | if (err) throw err; |
| 1711 | user.salt = salt; |
| 1712 | user.hash = hash; |
| 1713 | delete user.passtype; |
| 1714 | obj.db.SetUser(user); |
| 1715 | |
| 1716 | // Send the verification email |
| 1717 | 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); } |
| 1718 | }, 0); |
| 1719 | 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 }; |
| 1720 | 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. |
| 1721 | obj.parent.DispatchEvent(['*', 'server-users'], obj, event); |
| 1722 | } |
| 1723 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1724 | } |
| 1725 | }); |
| 1726 | } |
| 1727 | } |
| 1728 | }); |
| 1729 | } |
| 1730 | |
| 1731 | // Called to process an account password reset |
| 1732 | function handleResetPasswordRequest(req, res, direct) { |
| 1733 | const domain = checkUserIpAddress(req, res); |
| 1734 | if (domain == null) { return; } |
| 1735 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 1736 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 1737 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 1738 | |
| 1739 | // Decrypt any session data |
| 1740 | const sec = parent.decryptSessionData(req.session.e); |
| 1741 | |
| 1742 | // Check everything is ok |
| 1743 | const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false) || (sec.rtreset === true)); |
| 1744 | 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')) { |
| 1745 | parent.debug('web', 'handleResetPasswordRequest: checks failed'); |
| 1746 | delete req.session.e; |
| 1747 | delete req.session.u2f; |
| 1748 | delete req.session.loginmode; |
| 1749 | delete req.session.tuserid; |
| 1750 | delete req.session.tuser; |
| 1751 | delete req.session.tpass; |
| 1752 | delete req.session.temail; |
| 1753 | delete req.session.tsms; |
| 1754 | delete req.session.tmsg; |
| 1755 | delete req.session.tpush; |
| 1756 | delete req.session.messageid; |
| 1757 | delete req.session.passhint; |
| 1758 | delete req.session.cuserid; |
| 1759 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1760 | return; |
| 1761 | } |
| 1762 | |
| 1763 | // Authenticate the user |
| 1764 | obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) { |
| 1765 | if (userid) { |
| 1766 | // Login |
| 1767 | var user = obj.users[userid]; |
| 1768 | |
| 1769 | // If we have password requirements, check this here. |
| 1770 | if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) { |
| 1771 | parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)'); |
| 1772 | req.session.loginmode = 6; |
| 1773 | req.session.messageid = 105; // Password rejected, use a different one. |
| 1774 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1775 | return; |
| 1776 | } |
| 1777 | |
| 1778 | // Check if the password is the same as a previous one |
| 1779 | obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) { |
| 1780 | if (result != 0) { |
| 1781 | // This is the same password as an older one, request a password change again |
| 1782 | parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)'); |
| 1783 | req.session.loginmode = 6; |
| 1784 | req.session.messageid = 105; // Password rejected, use a different one. |
| 1785 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1786 | } else { |
| 1787 | // Update the password, use a different salt. |
| 1788 | require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) { |
| 1789 | const nowSeconds = Math.floor(Date.now() / 1000); |
| 1790 | if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; } |
| 1791 | |
| 1792 | if (domain.passwordrequirements != null) { |
| 1793 | // Save password hint if this feature is enabled |
| 1794 | 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; } |
| 1795 | |
| 1796 | // Save previous password if this feature is enabled |
| 1797 | if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) { |
| 1798 | if (user.oldpasswords == null) { user.oldpasswords = []; } |
| 1799 | user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds }); |
| 1800 | const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban; |
| 1801 | if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); } |
| 1802 | } |
| 1803 | } |
| 1804 | |
| 1805 | user.salt = salt; |
| 1806 | user.hash = hash; |
| 1807 | user.passchange = user.access = nowSeconds; |
| 1808 | delete user.passtype; |
| 1809 | obj.db.SetUser(user); |
| 1810 | |
| 1811 | // Event the account change |
| 1812 | var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id }; |
| 1813 | 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. |
| 1814 | obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); |
| 1815 | |
| 1816 | // Login successful |
| 1817 | parent.debug('web', 'handleResetPasswordRequest: success'); |
| 1818 | req.session.userid = userid; |
| 1819 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 1820 | setSessionRandom(req); |
| 1821 | const sec = parent.decryptSessionData(req.session.e); |
| 1822 | completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions); |
| 1823 | }, 0); |
| 1824 | } |
| 1825 | }, 0); |
| 1826 | } else { |
| 1827 | // Failed, error out. |
| 1828 | parent.debug('web', 'handleResetPasswordRequest: failed authenticate()'); |
| 1829 | delete req.session.e; |
| 1830 | delete req.session.u2f; |
| 1831 | delete req.session.loginmode; |
| 1832 | delete req.session.tuserid; |
| 1833 | delete req.session.tuser; |
| 1834 | delete req.session.tpass; |
| 1835 | delete req.session.temail; |
| 1836 | delete req.session.tsms; |
| 1837 | delete req.session.tmsg; |
| 1838 | delete req.session.tpush; |
| 1839 | delete req.session.messageid; |
| 1840 | delete req.session.passhint; |
| 1841 | delete req.session.cuserid; |
| 1842 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1843 | return; |
| 1844 | } |
| 1845 | }); |
| 1846 | } |
| 1847 | |
| 1848 | // Called to process an account reset request |
| 1849 | function handleResetAccountRequest(req, res, direct) { |
| 1850 | const domain = checkUserIpAddress(req, res); |
| 1851 | if (domain == null) { return; } |
| 1852 | const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false)); |
| 1853 | 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; } |
| 1854 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 1855 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 1856 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 1857 | |
| 1858 | // Always lowercase the email address |
| 1859 | if (req.body.email) { req.body.email = req.body.email.toLowerCase(); } |
| 1860 | |
| 1861 | // Get the email from the body or session. |
| 1862 | var email = req.body.email; |
| 1863 | if ((email == null) || (email == '')) { email = req.session.temail; } |
| 1864 | |
| 1865 | // Check the email string format |
| 1866 | if (!email || checkEmail(email) == false) { |
| 1867 | parent.debug('web', 'handleResetAccountRequest: Invalid email'); |
| 1868 | req.session.loginmode = 3; |
| 1869 | req.session.messageid = 106; // Invalid email. |
| 1870 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1871 | } else { |
| 1872 | obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) { |
| 1873 | // Remove all accounts that start with ~ since they are special accounts. |
| 1874 | var cleanDocs = []; |
| 1875 | if ((err == null) && (docs.length > 0)) { |
| 1876 | for (var i in docs) { |
| 1877 | const user = docs[i]; |
| 1878 | const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts |
| 1879 | const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts |
| 1880 | if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); } |
| 1881 | } |
| 1882 | } |
| 1883 | docs = cleanDocs; |
| 1884 | |
| 1885 | // Check if we have any account that match this email address |
| 1886 | if ((err != null) || (docs.length == 0)) { |
| 1887 | parent.debug('web', 'handleResetAccountRequest: Account not found'); |
| 1888 | req.session.loginmode = 3; |
| 1889 | 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. |
| 1890 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1891 | } else { |
| 1892 | // 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. |
| 1893 | var responseSent = false; |
| 1894 | for (var i in docs) { |
| 1895 | var user = docs[i]; |
| 1896 | if (checkUserOneTimePasswordRequired(domain, user, req) == true) { |
| 1897 | // Second factor setup, request it now. |
| 1898 | checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) { |
| 1899 | if (result == false) { |
| 1900 | if (i == 0) { |
| 1901 | |
| 1902 | // Check if 2FA is allowed for this IP address |
| 1903 | if (obj.checkAllow2Fa(req) == false) { |
| 1904 | // Wait and redirect the user |
| 1905 | setTimeout(function () { |
| 1906 | req.session.messageid = 114; // IP address blocked, try again later. |
| 1907 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1908 | }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); |
| 1909 | return; |
| 1910 | } |
| 1911 | |
| 1912 | // 2-step auth is required, but the token is not present or not valid. |
| 1913 | parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again'); |
| 1914 | if ((req.body.token != null) || (req.body.hwtoken != null)) { |
| 1915 | var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); |
| 1916 | var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null)); |
| 1917 | if ((req.body.hwtoken == '**sms**') && sms2fa) { |
| 1918 | // Cause a token to be sent to the user's phone number |
| 1919 | user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() }; |
| 1920 | obj.db.SetUser(user); |
| 1921 | parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone); |
| 1922 | parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req)); |
| 1923 | req.session.messageid = 4; // SMS sent. |
| 1924 | } else if ((req.body.hwtoken == '**msg**') && msg2fa) { |
| 1925 | // Cause a token to be sent to the user's messager account |
| 1926 | user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() }; |
| 1927 | obj.db.SetUser(user); |
| 1928 | parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle); |
| 1929 | parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req)); |
| 1930 | req.session.messageid = 6; // Message sent. |
| 1931 | } else { |
| 1932 | req.session.messageid = 108; // Invalid token, try again. |
| 1933 | const ua = obj.getUserAgentInfo(req); |
| 1934 | 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] }); |
| 1935 | obj.setbad2Fa(req); |
| 1936 | } |
| 1937 | } |
| 1938 | req.session.loginmode = 5; |
| 1939 | req.session.temail = email; |
| 1940 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1941 | } |
| 1942 | } else { |
| 1943 | // Send email to perform recovery. |
| 1944 | delete req.session.temail; |
| 1945 | if (domain.mailserver != null) { |
| 1946 | domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); |
| 1947 | if (i == 0) { |
| 1948 | parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.'); |
| 1949 | req.session.loginmode = 1; |
| 1950 | req.session.messageid = 1; // If valid, reset mail sent. |
| 1951 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1952 | } |
| 1953 | } else { |
| 1954 | if (i == 0) { |
| 1955 | parent.debug('web', 'handleResetAccountRequest: Unable to sent email.'); |
| 1956 | req.session.loginmode = 3; |
| 1957 | req.session.messageid = 109; // Unable to sent email. |
| 1958 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1959 | } |
| 1960 | } |
| 1961 | } |
| 1962 | }); |
| 1963 | } else { |
| 1964 | // No second factor, send email to perform recovery. |
| 1965 | if (domain.mailserver != null) { |
| 1966 | domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); |
| 1967 | if (i == 0) { |
| 1968 | parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.'); |
| 1969 | req.session.loginmode = 1; |
| 1970 | req.session.messageid = 1; // If valid, reset mail sent. |
| 1971 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1972 | } |
| 1973 | } else { |
| 1974 | if (i == 0) { |
| 1975 | parent.debug('web', 'handleResetAccountRequest: Unable to sent email.'); |
| 1976 | req.session.loginmode = 3; |
| 1977 | req.session.messageid = 109; // Unable to sent email. |
| 1978 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 1979 | } |
| 1980 | } |
| 1981 | } |
| 1982 | } |
| 1983 | } |
| 1984 | }); |
| 1985 | } |
| 1986 | } |
| 1987 | |
| 1988 | // Handle account email change and email verification request |
| 1989 | function handleCheckAccountEmailRequest(req, res, direct) { |
| 1990 | const domain = checkUserIpAddress(req, res); |
| 1991 | if (domain == null) { return; } |
| 1992 | 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; } |
| 1993 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 1994 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 1995 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 1996 | |
| 1997 | // Always lowercase the email address |
| 1998 | if (req.body.email) { req.body.email = req.body.email.toLowerCase(); } |
| 1999 | |
| 2000 | // Get the email from the body or session. |
| 2001 | var email = req.body.email; |
| 2002 | if ((email == null) || (email == '')) { email = req.session.temail; } |
| 2003 | |
| 2004 | // Check if this request is for an allows email domain |
| 2005 | if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) { |
| 2006 | var i = -1; |
| 2007 | if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); } |
| 2008 | if (i == -1) { |
| 2009 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)'); |
| 2010 | req.session.loginmode = 7; |
| 2011 | req.session.messageid = 106; // Invalid email. |
| 2012 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2013 | return; |
| 2014 | } |
| 2015 | var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase(); |
| 2016 | for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } } |
| 2017 | if (emailok == false) { |
| 2018 | parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)'); |
| 2019 | req.session.loginmode = 7; |
| 2020 | req.session.messageid = 106; // Invalid email. |
| 2021 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2022 | return; |
| 2023 | } |
| 2024 | } |
| 2025 | |
| 2026 | // Check the email string format |
| 2027 | if (!email || checkEmail(email) == false) { |
| 2028 | parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email'); |
| 2029 | req.session.loginmode = 7; |
| 2030 | req.session.messageid = 106; // Invalid email. |
| 2031 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2032 | } else { |
| 2033 | // Check is email already exists |
| 2034 | obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) { |
| 2035 | if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) { |
| 2036 | // Email already exists |
| 2037 | req.session.messageid = 102; // Existing account with this email address. |
| 2038 | } else { |
| 2039 | // Update the user and notify of user email address change |
| 2040 | var user = obj.users[req.session.cuserid]; |
| 2041 | if (user.email != email) { |
| 2042 | user.email = email; |
| 2043 | db.SetUser(user); |
| 2044 | var targets = ['*', 'server-users', user._id]; |
| 2045 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 2046 | var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id }; |
| 2047 | 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. |
| 2048 | parent.DispatchEvent(targets, obj, event); |
| 2049 | } |
| 2050 | |
| 2051 | // Send the verification email |
| 2052 | domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); |
| 2053 | |
| 2054 | // Send the response |
| 2055 | req.session.messageid = 2; // Email sent. |
| 2056 | } |
| 2057 | req.session.loginmode = 7; |
| 2058 | delete req.session.cuserid; |
| 2059 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2060 | }); |
| 2061 | } |
| 2062 | } |
| 2063 | |
| 2064 | // Called to process a web based email verification request |
| 2065 | function handleCheckMailRequest(req, res) { |
| 2066 | const domain = checkUserIpAddress(req, res); |
| 2067 | if (domain == null) { return; } |
| 2068 | if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; } |
| 2069 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2070 | |
| 2071 | if (req.query.c != null) { |
| 2072 | var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30); |
| 2073 | if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) { |
| 2074 | var idsplit = cookie.u.split('/'); |
| 2075 | if ((idsplit.length != 3) || (idsplit[1] != domain.id)) { |
| 2076 | parent.debug('web', 'handleCheckMailRequest: Invalid domain.'); |
| 2077 | 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)); |
| 2078 | } else { |
| 2079 | obj.db.Get(cookie.u, function (err, docs) { |
| 2080 | if (docs.length == 0) { |
| 2081 | parent.debug('web', 'handleCheckMailRequest: Invalid username.'); |
| 2082 | 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)); |
| 2083 | } else { |
| 2084 | var user = docs[0]; |
| 2085 | if (user.email != cookie.e) { |
| 2086 | parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.'); |
| 2087 | 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)); |
| 2088 | } else { |
| 2089 | if (cookie.a == 1) { |
| 2090 | // Account email verification |
| 2091 | if (user.emailVerified == true) { |
| 2092 | parent.debug('web', 'handleCheckMailRequest: email already verified.'); |
| 2093 | 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)); |
| 2094 | } else { |
| 2095 | obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) { |
| 2096 | if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) { |
| 2097 | parent.debug('web', 'handleCheckMailRequest: email already in use.'); |
| 2098 | 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)); |
| 2099 | } else { |
| 2100 | parent.debug('web', 'handleCheckMailRequest: email verification success.'); |
| 2101 | |
| 2102 | // Set the verified flag |
| 2103 | obj.users[user._id].emailVerified = true; |
| 2104 | user.emailVerified = true; |
| 2105 | obj.db.SetUser(user); |
| 2106 | |
| 2107 | // Event the change |
| 2108 | 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 }; |
| 2109 | 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. |
| 2110 | obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); |
| 2111 | |
| 2112 | // Send the confirmation page |
| 2113 | 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)); |
| 2114 | |
| 2115 | // Send a notification |
| 2116 | obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() }); |
| 2117 | |
| 2118 | // Send to authLog |
| 2119 | obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] }); |
| 2120 | } |
| 2121 | }); |
| 2122 | } |
| 2123 | } else if (cookie.a == 2) { |
| 2124 | // Account reset |
| 2125 | if (user.emailVerified != true) { |
| 2126 | parent.debug('web', 'handleCheckMailRequest: email not verified.'); |
| 2127 | 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)); |
| 2128 | } else { |
| 2129 | if (req.query.confirm == 1) { |
| 2130 | // Set a temporary password |
| 2131 | obj.crypto.randomBytes(16, function (err, buf) { |
| 2132 | var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join(''); |
| 2133 | require('./pass').hash(newpass, function (err, salt, hash, tag) { |
| 2134 | if (err) throw err; |
| 2135 | |
| 2136 | // Change the password |
| 2137 | var userinfo = obj.users[user._id]; |
| 2138 | userinfo.salt = salt; |
| 2139 | userinfo.hash = hash; |
| 2140 | delete userinfo.passtype; |
| 2141 | userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000); |
| 2142 | delete userinfo.passhint; |
| 2143 | obj.db.SetUser(userinfo); |
| 2144 | |
| 2145 | // Event the change |
| 2146 | 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 }; |
| 2147 | 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. |
| 2148 | obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); |
| 2149 | |
| 2150 | // Send the new password |
| 2151 | 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)); |
| 2152 | parent.debug('web', 'handleCheckMailRequest: send temporary password.'); |
| 2153 | |
| 2154 | // Send to authLog |
| 2155 | obj.parent.authLog('https', 'Performed account reset for user ' + user.name); |
| 2156 | }, 0); |
| 2157 | }); |
| 2158 | } else { |
| 2159 | // Display a link for the user to confirm password reset |
| 2160 | // 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. |
| 2161 | 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)); |
| 2162 | } |
| 2163 | } |
| 2164 | } else { |
| 2165 | 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)); |
| 2166 | } |
| 2167 | } |
| 2168 | } |
| 2169 | }); |
| 2170 | } |
| 2171 | } else { |
| 2172 | 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)); |
| 2173 | } |
| 2174 | } |
| 2175 | } |
| 2176 | |
| 2177 | // Called to process an agent invite GET/POST request |
| 2178 | function handleInviteRequest(req, res) { |
| 2179 | const domain = getDomain(req); |
| 2180 | if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; } |
| 2181 | if (domain.agentinvitecodes != true) { nice404(req, res); return; } |
| 2182 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2183 | 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 |
| 2184 | |
| 2185 | // Each for a device group that has this invite code. |
| 2186 | for (var i in obj.meshes) { |
| 2187 | 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)) { |
| 2188 | // Send invitation link, valid for 1 minute. |
| 2189 | res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + (req.query.hide ? ('&hide=' + encodeURIComponent(req.query.hide)) : '')); |
| 2190 | return; |
| 2191 | } |
| 2192 | } |
| 2193 | |
| 2194 | render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code |
| 2195 | } |
| 2196 | |
| 2197 | // Called to render the MSTSC (RDP) or SSH web page |
| 2198 | function handleMSTSCRequest(req, res, page) { |
| 2199 | const domain = getDomain(req); |
| 2200 | if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; } |
| 2201 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2202 | |
| 2203 | // Check if we are in maintenance mode |
| 2204 | if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) { |
| 2205 | 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)); |
| 2206 | return; |
| 2207 | } |
| 2208 | |
| 2209 | // Set features we want to send to this page |
| 2210 | var features = 0; |
| 2211 | if (domain.allowsavingdevicecredentials === false) { features |= 1; } |
| 2212 | |
| 2213 | // Get the logged in user if present |
| 2214 | var user = null; |
| 2215 | |
| 2216 | // If there is a login token, use that |
| 2217 | if (req.query.login != null) { |
| 2218 | var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout |
| 2219 | if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; } |
| 2220 | } |
| 2221 | |
| 2222 | // If no token, see if we have an active session |
| 2223 | if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; } |
| 2224 | |
| 2225 | // If still no user, see if we have a default user |
| 2226 | if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; } |
| 2227 | |
| 2228 | // No user login, exit now |
| 2229 | if (user == null) { res.sendStatus(401); return; } |
| 2230 | |
| 2231 | if (req.query.ws != null) { |
| 2232 | // This is a query with a websocket relay cookie, check that the cookie is valid and use it. |
| 2233 | var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout |
| 2234 | if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) { |
| 2235 | |
| 2236 | // Fetch the node from the database |
| 2237 | obj.db.Get(rcookie.nodeid, function (err, nodes) { |
| 2238 | if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; } |
| 2239 | const node = nodes[0]; |
| 2240 | |
| 2241 | // Check if we have SSH/RDP credentials for this device |
| 2242 | var serverCredentials = 0; |
| 2243 | if (domain.allowsavingdevicecredentials !== false) { |
| 2244 | if (page == 'ssh') { |
| 2245 | if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password |
| 2246 | else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password |
| 2247 | else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password. |
| 2248 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format |
| 2249 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format |
| 2250 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format |
| 2251 | } else { |
| 2252 | if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password in legacy format |
| 2253 | if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format |
| 2254 | } |
| 2255 | } |
| 2256 | |
| 2257 | // Render the page |
| 2258 | 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)); |
| 2259 | }); |
| 2260 | return; |
| 2261 | } |
| 2262 | } |
| 2263 | |
| 2264 | // Check the nodeid |
| 2265 | if (req.query.node != null) { |
| 2266 | var nodeidsplit = req.query.node.split('/'); |
| 2267 | if (nodeidsplit.length == 1) { |
| 2268 | req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly |
| 2269 | } else if (nodeidsplit.length == 3) { |
| 2270 | if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format |
| 2271 | } else { |
| 2272 | req.query.node = null; // Bad nodeid |
| 2273 | } |
| 2274 | } |
| 2275 | |
| 2276 | // If there is no nodeid, exit now |
| 2277 | if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; } |
| 2278 | |
| 2279 | // Fetch the node from the database |
| 2280 | obj.db.Get(req.query.node, function (err, nodes) { |
| 2281 | if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; } |
| 2282 | const node = nodes[0]; |
| 2283 | |
| 2284 | // Check access rights, must have remote control rights |
| 2285 | if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; } |
| 2286 | |
| 2287 | // Figure out the target port |
| 2288 | var port = 0, serverCredentials = false; |
| 2289 | if (page == 'ssh') { |
| 2290 | // SSH port |
| 2291 | port = 22; |
| 2292 | if (typeof node.sshport == 'number') { port = node.sshport; } |
| 2293 | |
| 2294 | // Check if we have SSH credentials for this device |
| 2295 | if (domain.allowsavingdevicecredentials !== false) { |
| 2296 | if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password |
| 2297 | else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password |
| 2298 | else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password. |
| 2299 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format |
| 2300 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format |
| 2301 | else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format |
| 2302 | } |
| 2303 | } else { |
| 2304 | // RDP port |
| 2305 | port = 3389; |
| 2306 | if (typeof node.rdpport == 'number') { port = node.rdpport; } |
| 2307 | |
| 2308 | // Check if we have RDP credentials for this device |
| 2309 | if (domain.allowsavingdevicecredentials !== false) { |
| 2310 | 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 |
| 2311 | if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format |
| 2312 | } |
| 2313 | } |
| 2314 | 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; } } |
| 2315 | |
| 2316 | // Generate a cookie and respond |
| 2317 | var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey); |
| 2318 | render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain)); |
| 2319 | }); |
| 2320 | } |
| 2321 | |
| 2322 | // Called to handle push-only requests |
| 2323 | function handleFirebasePushOnlyRelayRequest(req, res) { |
| 2324 | parent.debug('email', 'handleFirebasePushOnlyRelayRequest'); |
| 2325 | if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; } |
| 2326 | if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; } |
| 2327 | if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; } |
| 2328 | var data = null; |
| 2329 | try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; } |
| 2330 | if (typeof data != 'object') { res.sendStatus(404); return; } |
| 2331 | if (typeof data.pmt != 'string') { res.sendStatus(404); return; } |
| 2332 | if (typeof data.payload != 'object') { res.sendStatus(404); return; } |
| 2333 | if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; } |
| 2334 | if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; } |
| 2335 | if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; } |
| 2336 | if (typeof data.options != 'object') { res.sendStatus(404); return; } |
| 2337 | if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; } |
| 2338 | if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; } |
| 2339 | parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok'); |
| 2340 | obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) { |
| 2341 | if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); } |
| 2342 | }); |
| 2343 | } |
| 2344 | |
| 2345 | // Called to handle two-way push notification relay request |
| 2346 | function handleFirebaseRelayRequest(ws, req) { |
| 2347 | parent.debug('email', 'handleFirebaseRelayRequest'); |
| 2348 | if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; } |
| 2349 | if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; } |
| 2350 | if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; } |
| 2351 | 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; } |
| 2352 | obj.parent.firebase.setupRelay(ws); |
| 2353 | } |
| 2354 | |
| 2355 | // Called to process an agent invite request |
| 2356 | function handleAgentInviteRequest(req, res) { |
| 2357 | const domain = getDomain(req); |
| 2358 | if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; } |
| 2359 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2360 | |
| 2361 | if (req.query.c != null) { |
| 2362 | // A cookie is specified in the query string, use that |
| 2363 | var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey); |
| 2364 | if (cookie == null) { res.sendStatus(404); return; } |
| 2365 | var mesh = obj.meshes[cookie.mid]; |
| 2366 | if (mesh == null) { res.sendStatus(404); return; } |
| 2367 | var installflags = cookie.f; |
| 2368 | if (typeof installflags != 'number') { installflags = 0; } |
| 2369 | var showagents = cookie.ag; |
| 2370 | if (typeof showagents != 'number') { showagents = 0; } |
| 2371 | parent.debug('web', 'handleAgentInviteRequest using cookie.'); |
| 2372 | |
| 2373 | // Build the mobile agent URL, this is used to connect mobile devices |
| 2374 | var agentServerName = obj.getWebServerName(domain, req); |
| 2375 | if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; } |
| 2376 | var xdomain = (domain.dns == null) ? domain.id : ''; |
| 2377 | var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 2378 | if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that. |
| 2379 | if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that. |
| 2380 | var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2]; |
| 2381 | |
| 2382 | var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey); |
| 2383 | 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)); |
| 2384 | } else if (req.query.m != null) { |
| 2385 | // The MeshId is specified in the query string, use that |
| 2386 | var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()]; |
| 2387 | if (mesh == null) { res.sendStatus(404); return; } |
| 2388 | var installflags = 0; |
| 2389 | if (req.query.f) { installflags = parseInt(req.query.f); } |
| 2390 | if (typeof installflags != 'number') { installflags = 0; } |
| 2391 | var showagents = 0; |
| 2392 | if (req.query.f) { showagents = parseInt(req.query.ag); } |
| 2393 | if (typeof showagents != 'number') { showagents = 0; } |
| 2394 | parent.debug('web', 'handleAgentInviteRequest using meshid.'); |
| 2395 | |
| 2396 | // Build the mobile agent URL, this is used to connect mobile devices |
| 2397 | var agentServerName = obj.getWebServerName(domain, req); |
| 2398 | if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; } |
| 2399 | var xdomain = (domain.dns == null) ? domain.id : ''; |
| 2400 | var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 2401 | if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that. |
| 2402 | if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that. |
| 2403 | var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2]; |
| 2404 | |
| 2405 | var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey); |
| 2406 | 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)); |
| 2407 | } |
| 2408 | } |
| 2409 | |
| 2410 | // Called to process an agent invite request |
| 2411 | function handleUserImageRequest(req, res) { |
| 2412 | const domain = getDomain(req); |
| 2413 | if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; } |
| 2414 | if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; } |
| 2415 | var imageUserId = req.session.userid; |
| 2416 | if ((req.query.id != null)) { |
| 2417 | var user = obj.users[req.session.userid]; |
| 2418 | if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; } |
| 2419 | imageUserId = 'user/' + domain.id + '/' + req.query.id; |
| 2420 | } |
| 2421 | obj.db.Get('im' + imageUserId, function (err, docs) { |
| 2422 | if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; } |
| 2423 | var imagebase64 = docs[0].image; |
| 2424 | if (imagebase64.startsWith('data:image/png;base64,')) { |
| 2425 | res.set('Content-Type', 'image/png'); |
| 2426 | res.set({ 'Cache-Control': 'no-store' }); |
| 2427 | res.send(Buffer.from(imagebase64.substring(22), 'base64')); |
| 2428 | } else if (imagebase64.startsWith('data:image/jpeg;base64,')) { |
| 2429 | res.set('Content-Type', 'image/jpeg'); |
| 2430 | res.set({ 'Cache-Control': 'no-store' }); |
| 2431 | res.send(Buffer.from(imagebase64.substring(23), 'base64')); |
| 2432 | } else { |
| 2433 | res.sendStatus(404); |
| 2434 | } |
| 2435 | }); |
| 2436 | } |
| 2437 | |
| 2438 | function handleDeleteAccountRequest(req, res, direct) { |
| 2439 | parent.debug('web', 'handleDeleteAccountRequest()'); |
| 2440 | const domain = checkUserIpAddress(req, res); |
| 2441 | if (domain == null) { return; } |
| 2442 | if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; } |
| 2443 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2444 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 2445 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 2446 | |
| 2447 | var user = null; |
| 2448 | if (req.body.authcookie) { |
| 2449 | // If a authentication cookie is provided, decode it here |
| 2450 | var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 2451 | if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; } |
| 2452 | } else { |
| 2453 | // Check if the user is logged and we have all required parameters |
| 2454 | if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) { |
| 2455 | parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.'); |
| 2456 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2457 | return; |
| 2458 | } else { |
| 2459 | user = obj.users[req.session.userid]; |
| 2460 | } |
| 2461 | } |
| 2462 | if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; } |
| 2463 | if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; } |
| 2464 | |
| 2465 | // Check if the password is correct |
| 2466 | obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) { |
| 2467 | var deluser = obj.users[userid]; |
| 2468 | if ((userid != null) && (deluser != null)) { |
| 2469 | // Remove all links to this user |
| 2470 | if (deluser.links != null) { |
| 2471 | for (var i in deluser.links) { |
| 2472 | if (i.startsWith('mesh/')) { |
| 2473 | // Get the device group |
| 2474 | var mesh = obj.meshes[i]; |
| 2475 | if (mesh) { |
| 2476 | // Remove user from the mesh |
| 2477 | if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); } |
| 2478 | |
| 2479 | // Notify mesh change |
| 2480 | var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name; |
| 2481 | 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 }; |
| 2482 | 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. |
| 2483 | parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event); |
| 2484 | } |
| 2485 | } else if (i.startsWith('node/')) { |
| 2486 | // Get the node and the rights for this node |
| 2487 | obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) { |
| 2488 | if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return; |
| 2489 | |
| 2490 | // Remove the link and save the node to the database |
| 2491 | delete node.links[deluser._id]; |
| 2492 | if (Object.keys(node.links).length == 0) { delete node.links; } |
| 2493 | db.Set(obj.cleanDevice(node)); |
| 2494 | |
| 2495 | // Event the node change |
| 2496 | 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) } |
| 2497 | 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. |
| 2498 | parent.DispatchEvent(['*', node.meshid, node._id], obj, event); |
| 2499 | }); |
| 2500 | } else if (i.startsWith('ugrp/')) { |
| 2501 | // Get the device group |
| 2502 | var ugroup = obj.userGroups[i]; |
| 2503 | if (ugroup) { |
| 2504 | // Remove user from the user group |
| 2505 | if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); } |
| 2506 | |
| 2507 | // Notify user group change |
| 2508 | var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name; |
| 2509 | 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 }; |
| 2510 | 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. |
| 2511 | parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event); |
| 2512 | } |
| 2513 | } |
| 2514 | } |
| 2515 | } |
| 2516 | |
| 2517 | obj.db.Remove('ws' + deluser._id); // Remove user web state |
| 2518 | obj.db.Remove('nt' + deluser._id); // Remove notes for this user |
| 2519 | obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user |
| 2520 | obj.db.Remove('im' + deluser._id); // Remove image for this user |
| 2521 | |
| 2522 | // Delete any login tokens |
| 2523 | parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) { |
| 2524 | if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } } |
| 2525 | }); |
| 2526 | |
| 2527 | // Delete all files on the server for this account |
| 2528 | try { |
| 2529 | var deluserpath = obj.getServerRootFilePath(deluser); |
| 2530 | if (deluserpath != null) { obj.deleteFolderRec(deluserpath); } |
| 2531 | } catch (e) { } |
| 2532 | |
| 2533 | // Remove the user |
| 2534 | obj.db.Remove(deluser._id); |
| 2535 | delete obj.users[deluser._id]; |
| 2536 | req.session = null; |
| 2537 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2538 | obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id }); |
| 2539 | parent.debug('web', 'handleDeleteAccountRequest: removed user.'); |
| 2540 | } else { |
| 2541 | parent.debug('web', 'handleDeleteAccountRequest: auth failed.'); |
| 2542 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2543 | } |
| 2544 | }); |
| 2545 | } |
| 2546 | |
| 2547 | // Check a user's password |
| 2548 | obj.checkUserPassword = function (domain, user, password, func) { |
| 2549 | // Check the old password |
| 2550 | if (user.passtype != null) { |
| 2551 | // IIS default clear or weak password hashing (SHA-1) |
| 2552 | require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) { |
| 2553 | if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); } |
| 2554 | if (hash == user.hash) { |
| 2555 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked |
| 2556 | parent.debug('web', 'checkUserPassword: SHA-1 ok.'); |
| 2557 | return func(true); // Allow password change |
| 2558 | } |
| 2559 | func(false); |
| 2560 | }); |
| 2561 | } else { |
| 2562 | // Default strong password hashing (pbkdf2 SHA384) |
| 2563 | require('./pass').hash(password, user.salt, function (err, hash, tag) { |
| 2564 | if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); } |
| 2565 | if (hash == user.hash) { |
| 2566 | if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked |
| 2567 | parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.'); |
| 2568 | return func(true); // Allow password change |
| 2569 | } |
| 2570 | func(false); |
| 2571 | }, 0); |
| 2572 | } |
| 2573 | } |
| 2574 | |
| 2575 | // Check a user's old passwords |
| 2576 | // Callback: 0=OK, 1=OldPass, 2=CommonPass |
| 2577 | obj.checkOldUserPasswords = function (domain, user, password, func) { |
| 2578 | // Check how many old passwords we need to check |
| 2579 | if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) { |
| 2580 | if (user.oldpasswords != null) { |
| 2581 | const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban; |
| 2582 | if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); } |
| 2583 | } |
| 2584 | } else { |
| 2585 | delete user.oldpasswords; |
| 2586 | } |
| 2587 | |
| 2588 | // If there is no old passwords, exit now. |
| 2589 | var oldPassCount = 1; |
| 2590 | if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; } |
| 2591 | var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func }; |
| 2592 | |
| 2593 | // Test against common passwords if this feature is enabled |
| 2594 | // Example of common passwords: 123456789, password123 |
| 2595 | if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) { |
| 2596 | oldPassCheckState.count++; |
| 2597 | require('wildleek')(password).then(function (wild) { |
| 2598 | if (wild == true) { oldPassCheckState.response = 2; } |
| 2599 | if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); } |
| 2600 | }); |
| 2601 | } |
| 2602 | |
| 2603 | // Try current password |
| 2604 | require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) { |
| 2605 | if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; } |
| 2606 | if (--tag.count == 0) { tag.func(tag.response); } |
| 2607 | }, oldPassCheckState); |
| 2608 | |
| 2609 | // Try each old password |
| 2610 | if (user.oldpasswords != null) { |
| 2611 | for (var i in user.oldpasswords) { |
| 2612 | const oldpassword = user.oldpasswords[i]; |
| 2613 | // Default strong password hashing (pbkdf2 SHA384) |
| 2614 | require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) { |
| 2615 | if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; } |
| 2616 | if (--tag.state.count == 0) { tag.state.func(tag.state.response); } |
| 2617 | }, { oldPassword: oldpassword, state: oldPassCheckState }); |
| 2618 | } |
| 2619 | } |
| 2620 | } |
| 2621 | |
| 2622 | // Handle password changes |
| 2623 | function handlePasswordChangeRequest(req, res, direct) { |
| 2624 | const domain = checkUserIpAddress(req, res); |
| 2625 | if (domain == null) { return; } |
| 2626 | if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; } |
| 2627 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2628 | if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token |
| 2629 | if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed |
| 2630 | |
| 2631 | // Check if the user is logged and we have all required parameters |
| 2632 | 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)) { |
| 2633 | parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).'); |
| 2634 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2635 | return; |
| 2636 | } |
| 2637 | |
| 2638 | // Get the current user |
| 2639 | var user = obj.users[req.session.userid]; |
| 2640 | if (!user) { |
| 2641 | parent.debug('web', 'handlePasswordChangeRequest: user not found.'); |
| 2642 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2643 | return; |
| 2644 | } |
| 2645 | |
| 2646 | // Check account settings locked |
| 2647 | if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { |
| 2648 | parent.debug('web', 'handlePasswordChangeRequest: account settings locked.'); |
| 2649 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2650 | return; |
| 2651 | } |
| 2652 | |
| 2653 | // Check old password |
| 2654 | obj.checkUserPassword(domain, user, req.body.apassword1, function (result) { |
| 2655 | if (result == true) { |
| 2656 | // Check if the new password is allowed, only do this if this feature is enabled. |
| 2657 | parent.checkOldUserPasswords(domain, user, command.newpass, function (result) { |
| 2658 | if (result == 1) { |
| 2659 | parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.'); |
| 2660 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2661 | } else if (result == 2) { |
| 2662 | parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.'); |
| 2663 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2664 | } else { |
| 2665 | // Update the password |
| 2666 | require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) { |
| 2667 | const nowSeconds = Math.floor(Date.now() / 1000); |
| 2668 | if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; } |
| 2669 | if (domain.passwordrequirements != null) { |
| 2670 | // Save password hint if this feature is enabled |
| 2671 | 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; } |
| 2672 | |
| 2673 | // Save previous password if this feature is enabled |
| 2674 | if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) { |
| 2675 | if (user.oldpasswords == null) { user.oldpasswords = []; } |
| 2676 | user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds }); |
| 2677 | const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban; |
| 2678 | if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); } |
| 2679 | } |
| 2680 | } |
| 2681 | user.salt = salt; |
| 2682 | user.hash = hash; |
| 2683 | user.passchange = user.access = nowSeconds; |
| 2684 | delete user.passtype; |
| 2685 | |
| 2686 | obj.db.SetUser(user); |
| 2687 | req.session.viewmode = 2; |
| 2688 | if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } |
| 2689 | 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 }); |
| 2690 | }, 0); |
| 2691 | } |
| 2692 | }); |
| 2693 | } |
| 2694 | }); |
| 2695 | } |
| 2696 | |
| 2697 | // Called when a strategy login occurred |
| 2698 | // This is called after a successful Oauth to Twitter, Google, GitHub... |
| 2699 | function handleStrategyLogin(req, res) { |
| 2700 | const domain = checkUserIpAddress(req, res); |
| 2701 | if (domain == null) { return; } |
| 2702 | if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) { |
| 2703 | const strategy = domain.authstrategies[req.user.strategy]; |
| 2704 | const groups = { 'enabled': typeof strategy.groups == 'object' } |
| 2705 | parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`); |
| 2706 | if (groups.enabled) { // Groups only available for OIDC strategy currently |
| 2707 | groups.userMemberships = obj.common.convertStrArray(req.user.groups); |
| 2708 | groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false; |
| 2709 | groups.syncMemberships = []; |
| 2710 | groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false; |
| 2711 | groups.grantAdmin = false; |
| 2712 | groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true; |
| 2713 | groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required); |
| 2714 | groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin); |
| 2715 | groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter); |
| 2716 | |
| 2717 | // Fancy Logs |
| 2718 | let groupMessage = ''; |
| 2719 | if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` } |
| 2720 | else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` } |
| 2721 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage); |
| 2722 | |
| 2723 | // Check user membership in required groups |
| 2724 | if (groups.requiredGroups.length > 0) { |
| 2725 | let match = false |
| 2726 | for (var i in groups.requiredGroups) { |
| 2727 | if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) { |
| 2728 | match = true; |
| 2729 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`); |
| 2730 | } |
| 2731 | } |
| 2732 | if (match === false) { |
| 2733 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`); |
| 2734 | req.session.loginmode = 1; |
| 2735 | req.session.messageid = 111; // Access Denied. |
| 2736 | res.redirect(domain.url + getQueryPortion(req)); |
| 2737 | return; |
| 2738 | } |
| 2739 | } |
| 2740 | |
| 2741 | // Check user membership in admin groups |
| 2742 | if (groups.siteAdminEnabled === true) { |
| 2743 | groups.grantAdmin = false; |
| 2744 | for (var i in strategy.groups.siteadmin) { |
| 2745 | if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) { |
| 2746 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`); |
| 2747 | groups.siteAdmin = strategy.groups.siteadmin[i]; |
| 2748 | groups.grantAdmin = true; |
| 2749 | break; |
| 2750 | } |
| 2751 | } |
| 2752 | } |
| 2753 | |
| 2754 | // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral) |
| 2755 | if (groups.syncEnabled === true) { |
| 2756 | if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it |
| 2757 | for (var i in groups.syncFilter) { |
| 2758 | if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); } |
| 2759 | } |
| 2760 | } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead |
| 2761 | for (var i in groups.userMemberships) { |
| 2762 | groups.syncMemberships.push(groups.userMemberships[i]); |
| 2763 | } |
| 2764 | } |
| 2765 | if (groups.syncMemberships.length > 0) { |
| 2766 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`); |
| 2767 | } else { |
| 2768 | groups.syncMemberships = null; |
| 2769 | groups.syncEnabled = false; |
| 2770 | if (groups.syncFilter.length > 0){ |
| 2771 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`); |
| 2772 | } else { |
| 2773 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`); |
| 2774 | } |
| 2775 | } |
| 2776 | } |
| 2777 | } |
| 2778 | |
| 2779 | // Check if the user already exists |
| 2780 | const userid = 'user/' + domain.id + '/' + req.user.sid; |
| 2781 | var user = obj.users[userid]; |
| 2782 | if (user == null) { |
| 2783 | var newAccountAllowed = false; |
| 2784 | var newAccountRealms = null; |
| 2785 | |
| 2786 | if (domain.newaccounts === true) { newAccountAllowed = true; } |
| 2787 | if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; } |
| 2788 | |
| 2789 | if (domain.authstrategies[req.user.strategy]) { |
| 2790 | if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; } |
| 2791 | if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; } |
| 2792 | } |
| 2793 | |
| 2794 | if (newAccountAllowed === true) { |
| 2795 | // Create the user |
| 2796 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`); |
| 2797 | 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 }; |
| 2798 | if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; } |
| 2799 | if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights. |
| 2800 | 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. |
| 2801 | if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms). |
| 2802 | obj.users[userid] = user; |
| 2803 | |
| 2804 | // Auto-join any user groups |
| 2805 | var newaccountsusergroups = null; |
| 2806 | if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; } |
| 2807 | if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; } |
| 2808 | if (newaccountsusergroups) { |
| 2809 | for (var i in newaccountsusergroups) { |
| 2810 | var ugrpid = newaccountsusergroups[i]; |
| 2811 | if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; } |
| 2812 | var ugroup = obj.userGroups[ugrpid]; |
| 2813 | if (ugroup != null) { |
| 2814 | // Add group to the user |
| 2815 | if (user.links == null) { user.links = {}; } |
| 2816 | user.links[ugroup._id] = { rights: 1 }; |
| 2817 | |
| 2818 | // Add user to the group |
| 2819 | ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 }; |
| 2820 | db.Set(ugroup); |
| 2821 | |
| 2822 | // Notify user group change |
| 2823 | 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 }; |
| 2824 | 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. |
| 2825 | parent.DispatchEvent(['*', ugroup._id, user._id], obj, event); |
| 2826 | } |
| 2827 | } |
| 2828 | } |
| 2829 | |
| 2830 | if (groups.enabled === true) { |
| 2831 | // Sync the user groups if enabled |
| 2832 | if (groups.syncEnabled === true) { |
| 2833 | // Set groupType to the preset name if it exists, otherwise use the strategy name |
| 2834 | const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy; |
| 2835 | syncExternalUserGroups(domain, user, groups.syncMemberships, groupType); |
| 2836 | } |
| 2837 | // See if the user is a member of the site admin group. |
| 2838 | if (groups.grantAdmin === true) { |
| 2839 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`); |
| 2840 | user.siteadmin = 0xFFFFFFFF; |
| 2841 | } |
| 2842 | } |
| 2843 | |
| 2844 | // Save the user |
| 2845 | obj.db.SetUser(user); |
| 2846 | |
| 2847 | // Event user creation |
| 2848 | var targets = ['*', 'server-users']; |
| 2849 | 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 }; |
| 2850 | 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. |
| 2851 | parent.DispatchEvent(targets, obj, event); |
| 2852 | |
| 2853 | req.session.userid = userid; |
| 2854 | setSessionRandom(req); |
| 2855 | |
| 2856 | // Notify account login using SSO |
| 2857 | var targets = ['*', 'server-users', user._id]; |
| 2858 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 2859 | const ua = obj.getUserAgentInfo(req); |
| 2860 | 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' }; |
| 2861 | obj.parent.DispatchEvent(targets, obj, loginEvent); |
| 2862 | } else { |
| 2863 | // New users not allowed |
| 2864 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`); |
| 2865 | req.session.loginmode = 1; |
| 2866 | req.session.messageid = 100; // Unable to create account. |
| 2867 | res.redirect(domain.url + getQueryPortion(req)); |
| 2868 | return; |
| 2869 | } |
| 2870 | } else { // Login success |
| 2871 | // Check for basic changes |
| 2872 | var userChanged = false; |
| 2873 | if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; } |
| 2874 | if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; } |
| 2875 | |
| 2876 | if (groups.enabled === true) { |
| 2877 | // Sync the user groups if enabled |
| 2878 | if (groups.syncEnabled === true) { |
| 2879 | syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy) |
| 2880 | } |
| 2881 | // See if the user is a member of the site admin group. |
| 2882 | if (groups.siteAdminEnabled === true) { |
| 2883 | if (groups.grantAdmin === true) { |
| 2884 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`); |
| 2885 | if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; } |
| 2886 | } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) { |
| 2887 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`); |
| 2888 | delete user.siteadmin; |
| 2889 | userChanged = true; |
| 2890 | } |
| 2891 | } |
| 2892 | } |
| 2893 | |
| 2894 | // Update db record for user if there are changes detected |
| 2895 | if (userChanged) { |
| 2896 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`); |
| 2897 | obj.db.SetUser(user); |
| 2898 | |
| 2899 | // Event user change |
| 2900 | var targets = ['*', 'server-users']; |
| 2901 | var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id }; |
| 2902 | 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. |
| 2903 | parent.DispatchEvent(targets, obj, event); |
| 2904 | } |
| 2905 | req.session.userid = userid; |
| 2906 | setSessionRandom(req); |
| 2907 | |
| 2908 | // Notify account login using SSO |
| 2909 | var targets = ['*', 'server-users', user._id]; |
| 2910 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 2911 | const ua = obj.getUserAgentInfo(req); |
| 2912 | 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' }; |
| 2913 | obj.parent.DispatchEvent(targets, obj, loginEvent); |
| 2914 | parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`); |
| 2915 | } |
| 2916 | } else if (req.session && req.session.userid && obj.users[req.session.userid]) { |
| 2917 | parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`); |
| 2918 | } else { |
| 2919 | parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`); |
| 2920 | } |
| 2921 | //res.redirect(domain.url); // This does not handle cookie correctly. |
| 2922 | res.set('Content-Type', 'text/html'); |
| 2923 | let url = domain.url; |
| 2924 | if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); } |
| 2925 | |
| 2926 | // check for relaystate is set, test against configured server name and accepted query params |
| 2927 | if(req.body && req.body.RelayState !== undefined){ |
| 2928 | var relayState = decodeURIComponent(req.body.RelayState); |
| 2929 | var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.'); |
| 2930 | |
| 2931 | var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` + |
| 2932 | `.*((?<=([\\?&])gotodevicename=(.{64})|` + |
| 2933 | `gotonode=(.{64})|` + |
| 2934 | `gotodeviceip=(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})|` + |
| 2935 | `gotodeviceip=(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:)` + |
| 2936 | `lang=(.{5})|` + |
| 2937 | `sitestyle=(\\d+)|` + |
| 2938 | `user=(.{64})|` + |
| 2939 | `pass=(.{256})|` + |
| 2940 | `key=|` + |
| 2941 | `locale=|` + |
| 2942 | `gotomesh=(.{64})|` + |
| 2943 | `gotouser=(.{0,64})|` + |
| 2944 | `gotougrp=(.{64})|` + |
| 2945 | `debug=|` + |
| 2946 | `filter=|` + |
| 2947 | `webrtc=|` + |
| 2948 | `hide=|` + |
| 2949 | `viewmode=(\\d+)(?=[\\&]|\\b)))`; |
| 2950 | |
| 2951 | var regex = new RegExp(regexstr); |
| 2952 | if(regex.test(relayState)){ |
| 2953 | url = relayState; |
| 2954 | } |
| 2955 | } |
| 2956 | |
| 2957 | res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>'); |
| 2958 | } |
| 2959 | |
| 2960 | // Indicates that any request to "/" should render "default" or "login" depending on login state |
| 2961 | function handleRootRequest(req, res, direct) { |
| 2962 | const domain = checkUserIpAddress(req, res); |
| 2963 | if (domain == null) { return; } |
| 2964 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 2965 | if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; } |
| 2966 | |
| 2967 | // If a HTTP header is required, check new UserRequiredHttpHeader |
| 2968 | 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; } } |
| 2969 | |
| 2970 | // If the session is expired, clear it. |
| 2971 | 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]; } } |
| 2972 | |
| 2973 | // Check if we are in maintenance mode |
| 2974 | if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) { |
| 2975 | parent.debug('web', 'handleLoginRequest: Server under maintenance.'); |
| 2976 | 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)); |
| 2977 | return; |
| 2978 | } |
| 2979 | |
| 2980 | // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used |
| 2981 | if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) { |
| 2982 | var q = new URL(req.url, 'http://localhost'); |
| 2983 | if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; } |
| 2984 | } |
| 2985 | |
| 2986 | if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) { |
| 2987 | // Login using SSPI |
| 2988 | domain.sspi.authenticate(req, res, function (err) { |
| 2989 | if ((err != null) || (req.connection.user == null)) { |
| 2990 | obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] }); |
| 2991 | parent.debug('web', 'handleRootRequest: SSPI auth required.'); |
| 2992 | try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request. |
| 2993 | } else { |
| 2994 | parent.debug('web', 'handleRootRequest: SSPI auth ok.'); |
| 2995 | handleRootRequestEx(req, res, domain, direct); |
| 2996 | } |
| 2997 | }); |
| 2998 | } else if (req.query.user && req.query.pass) { |
| 2999 | // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option. |
| 3000 | obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) { |
| 3001 | // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails. |
| 3002 | var user = obj.users[userid]; |
| 3003 | if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) { |
| 3004 | handleRootRequestEx(req, res, domain, direct); |
| 3005 | } else if ((userid != null) && (err == null)) { |
| 3006 | // Login success |
| 3007 | parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.'); |
| 3008 | req.session.userid = userid; |
| 3009 | delete req.session.currentNode; |
| 3010 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 3011 | setSessionRandom(req); |
| 3012 | obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x }); |
| 3013 | handleRootRequestEx(req, res, domain, direct); |
| 3014 | } else { |
| 3015 | // Login failed |
| 3016 | handleRootRequestEx(req, res, domain, direct); |
| 3017 | } |
| 3018 | }); |
| 3019 | } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) { |
| 3020 | // Check if the loginToken is still valid |
| 3021 | obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) { |
| 3022 | if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } } |
| 3023 | handleRootRequestEx(req, res, domain, direct); // Login using a different system |
| 3024 | }); |
| 3025 | } else { |
| 3026 | // Login using a different system |
| 3027 | handleRootRequestEx(req, res, domain, direct); |
| 3028 | } |
| 3029 | } |
| 3030 | |
| 3031 | function handleRootRequestEx(req, res, domain, direct) { |
| 3032 | var nologout = false, user = null; |
| 3033 | res.set({ 'Cache-Control': 'no-store' }); |
| 3034 | |
| 3035 | // Check if we have an incomplete domain name in the path |
| 3036 | if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) { |
| 3037 | parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.'); |
| 3038 | res.redirect(domain.url + getQueryPortion(req)); // BAD*** |
| 3039 | return; |
| 3040 | } |
| 3041 | |
| 3042 | if (obj.args.nousers == true) { |
| 3043 | // If in single user mode, setup things here. |
| 3044 | delete req.session.loginmode; |
| 3045 | req.session.userid = 'user/' + domain.id + '/~'; |
| 3046 | delete req.session.currentNode; |
| 3047 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 3048 | setSessionRandom(req); |
| 3049 | if (obj.users[req.session.userid] == null) { |
| 3050 | // Create the dummy user ~ with impossible password |
| 3051 | parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.'); |
| 3052 | obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 }; |
| 3053 | obj.db.SetUser(obj.users[req.session.userid]); |
| 3054 | } |
| 3055 | } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) { |
| 3056 | // If a default user is active, setup the session here. |
| 3057 | parent.debug('web', 'handleRootRequestEx: auth using default user.'); |
| 3058 | delete req.session.loginmode; |
| 3059 | req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); |
| 3060 | delete req.session.currentNode; |
| 3061 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 3062 | setSessionRandom(req); |
| 3063 | } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) { |
| 3064 | var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 3065 | //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here. |
| 3066 | if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) { |
| 3067 | // If a login cookie was provided, setup the session here. |
| 3068 | parent.debug('web', 'handleRootRequestEx: cookie auth ok.'); |
| 3069 | delete req.session.loginmode; |
| 3070 | req.session.userid = loginCookie.u; |
| 3071 | delete req.session.currentNode; |
| 3072 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 3073 | setSessionRandom(req); |
| 3074 | } else { |
| 3075 | parent.debug('web', 'handleRootRequestEx: cookie auth failed.'); |
| 3076 | } |
| 3077 | } else if (domain.sspi != null) { |
| 3078 | // SSPI login (Windows only) |
| 3079 | //console.log(req.connection.user, req.connection.userSid); |
| 3080 | if ((req.connection.user == null) || (req.connection.userSid == null)) { |
| 3081 | parent.debug('web', 'handleRootRequestEx: SSPI no user auth.'); |
| 3082 | res.sendStatus(404); return; |
| 3083 | } else { |
| 3084 | nologout = true; |
| 3085 | req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase(); |
| 3086 | req.session.usersid = req.connection.userSid; |
| 3087 | req.session.usersGroups = req.connection.userGroups; |
| 3088 | delete req.session.currentNode; |
| 3089 | req.session.ip = req.clientIp; // Bind this session to the IP address of the request |
| 3090 | setSessionRandom(req); |
| 3091 | 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 }); |
| 3092 | |
| 3093 | // Check if this user exists, create it if not. |
| 3094 | user = obj.users[req.session.userid]; |
| 3095 | if ((user == null) || (user.sid != req.session.usersid)) { |
| 3096 | // Create the domain user |
| 3097 | 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) }; |
| 3098 | if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; } |
| 3099 | if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; } |
| 3100 | for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } } |
| 3101 | if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin. |
| 3102 | |
| 3103 | // Auto-join any user groups |
| 3104 | if (typeof domain.newaccountsusergroups == 'object') { |
| 3105 | for (var i in domain.newaccountsusergroups) { |
| 3106 | var ugrpid = domain.newaccountsusergroups[i]; |
| 3107 | if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; } |
| 3108 | var ugroup = obj.userGroups[ugrpid]; |
| 3109 | if (ugroup != null) { |
| 3110 | // Add group to the user |
| 3111 | if (user2.links == null) { user2.links = {}; } |
| 3112 | user2.links[ugroup._id] = { rights: 1 }; |
| 3113 | |
| 3114 | // Add user to the group |
| 3115 | ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 }; |
| 3116 | db.Set(ugroup); |
| 3117 | |
| 3118 | // Notify user group change |
| 3119 | 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 }; |
| 3120 | 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. |
| 3121 | parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event); |
| 3122 | } |
| 3123 | } |
| 3124 | } |
| 3125 | |
| 3126 | obj.users[req.session.userid] = user2; |
| 3127 | obj.db.SetUser(user2); |
| 3128 | 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 }; |
| 3129 | 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. |
| 3130 | obj.parent.DispatchEvent(['*', 'server-users'], obj, event); |
| 3131 | parent.debug('web', 'handleRootRequestEx: SSPI new domain user.'); |
| 3132 | } |
| 3133 | } |
| 3134 | } |
| 3135 | |
| 3136 | // Figure out the minimal password requirement |
| 3137 | var passRequirements = null; |
| 3138 | if (domain.passwordrequirements != null) { |
| 3139 | if (domain.passrequirementstr == null) { |
| 3140 | var passRequirements = {}; |
| 3141 | if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; } |
| 3142 | if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; } |
| 3143 | if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; } |
| 3144 | if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; } |
| 3145 | if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; } |
| 3146 | if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; } |
| 3147 | domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements)); |
| 3148 | } |
| 3149 | passRequirements = domain.passwordrequirementsstr; |
| 3150 | } |
| 3151 | |
| 3152 | // If a user exists and is logged in, serve the default app, otherwise server the login app. |
| 3153 | if (req.session && req.session.userid && obj.users[req.session.userid]) { |
| 3154 | const user = obj.users[req.session.userid]; |
| 3155 | |
| 3156 | // Check if we are in maintenance mode |
| 3157 | if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) { |
| 3158 | req.session.messageid = 115; // Server under maintenance |
| 3159 | req.session.loginmode = 1; |
| 3160 | res.redirect(domain.url); |
| 3161 | return; |
| 3162 | } |
| 3163 | |
| 3164 | // If the request has a "meshmessengerid", redirect to MeshMessenger |
| 3165 | // This situation happens when you get a push notification for a chat session, but are not logged in. |
| 3166 | if (req.query.meshmessengerid != null) { |
| 3167 | res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '')); |
| 3168 | return; |
| 3169 | } |
| 3170 | |
| 3171 | const xdbGetFunc = function dbGetFunc(err, states) { |
| 3172 | if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain |
| 3173 | parent.debug('web', 'handleRootRequestEx: incorrect domain.'); |
| 3174 | dbGetFunc.req.session = null; |
| 3175 | dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD*** |
| 3176 | return; |
| 3177 | } |
| 3178 | |
| 3179 | // Check if this is a locked account |
| 3180 | if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) { |
| 3181 | // Locked account |
| 3182 | parent.debug('web', 'handleRootRequestEx: locked account.'); |
| 3183 | delete dbGetFunc.req.session.userid; |
| 3184 | delete dbGetFunc.req.session.currentNode; |
| 3185 | delete dbGetFunc.req.session.passhint; |
| 3186 | delete dbGetFunc.req.session.cuserid; |
| 3187 | dbGetFunc.req.session.messageid = 110; // Account locked. |
| 3188 | dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD*** |
| 3189 | return; |
| 3190 | } |
| 3191 | |
| 3192 | var viewmode = 1; |
| 3193 | if (dbGetFunc.req.session.viewmode) { |
| 3194 | viewmode = dbGetFunc.req.session.viewmode; |
| 3195 | delete dbGetFunc.req.session.viewmode; |
| 3196 | } else if (dbGetFunc.req.query.viewmode) { |
| 3197 | viewmode = dbGetFunc.req.query.viewmode; |
| 3198 | } |
| 3199 | var currentNode = ''; |
| 3200 | if (dbGetFunc.req.session.currentNode) { |
| 3201 | currentNode = dbGetFunc.req.session.currentNode; |
| 3202 | delete dbGetFunc.req.session.currentNode; |
| 3203 | } else if (dbGetFunc.req.query.node) { |
| 3204 | currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node; |
| 3205 | } |
| 3206 | var logoutcontrols = {}; |
| 3207 | if (obj.args.nousers != true) { logoutcontrols.name = user.name; } |
| 3208 | |
| 3209 | // Give the web page a list of supported server features for this domain and user |
| 3210 | const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req); |
| 3211 | |
| 3212 | // Create a authentication cookie |
| 3213 | const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey); |
| 3214 | const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey); |
| 3215 | |
| 3216 | // Send the main web application |
| 3217 | var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : ''; |
| 3218 | 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 |
| 3219 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 3220 | |
| 3221 | // Clean up the U2F challenge if needed |
| 3222 | if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; }; |
| 3223 | if (dbGetFunc.req.session.e) { |
| 3224 | const sec = parent.decryptSessionData(dbGetFunc.req.session.e); |
| 3225 | if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); } |
| 3226 | } |
| 3227 | |
| 3228 | // Intel AMT Scanning options |
| 3229 | var amtscanoptions = ''; |
| 3230 | if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); } |
| 3231 | else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); } |
| 3232 | |
| 3233 | // Fetch the web state |
| 3234 | parent.debug('web', 'handleRootRequestEx: success.'); |
| 3235 | |
| 3236 | var webstate = '{}'; |
| 3237 | if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); } |
| 3238 | if ((webstate == '{}') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults. |
| 3239 | if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it. |
| 3240 | var webstate2 = {}; |
| 3241 | try { if (webstate != '{}') { webstate2 = JSON.parse(webstate); } } catch (ex) { } |
| 3242 | for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; } |
| 3243 | webstate = JSON.stringify(webstate2); |
| 3244 | } |
| 3245 | |
| 3246 | // Custom user interface |
| 3247 | var customui = ''; |
| 3248 | if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); } |
| 3249 | |
| 3250 | // Custom files (CSS and JS) |
| 3251 | var customFiles = ''; |
| 3252 | if (domain.customFiles != null) { |
| 3253 | customFiles = encodeURIComponent(JSON.stringify(domain.customFiles)); |
| 3254 | } else if (domain.customfiles != null) { |
| 3255 | customFiles = encodeURIComponent(JSON.stringify(domain.customfiles)); |
| 3256 | } |
| 3257 | |
| 3258 | // Server features |
| 3259 | var serverFeatures = 255; |
| 3260 | if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab |
| 3261 | else if (typeof domain.myserver == 'object') { |
| 3262 | if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups |
| 3263 | if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore |
| 3264 | if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade |
| 3265 | if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log |
| 3266 | if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console |
| 3267 | if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing |
| 3268 | if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration |
| 3269 | } |
| 3270 | if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system. |
| 3271 | if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups |
| 3272 | if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore |
| 3273 | } |
| 3274 | |
| 3275 | // Get WebRTC configuration |
| 3276 | var webRtcConfig = null; |
| 3277 | if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); } |
| 3278 | else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); } |
| 3279 | |
| 3280 | // Load default page style or new modern ui |
| 3281 | var uiViewMode = 'default'; |
| 3282 | var webstateJSON = JSON.parse(webstate); |
| 3283 | if (req.query.sitestyle != null) { |
| 3284 | if (req.query.sitestyle == 3) { uiViewMode = 'default3'; } |
| 3285 | } else if (webstateJSON && webstateJSON.uiViewMode == 3) { |
| 3286 | uiViewMode = 'default3'; |
| 3287 | } else if (domain.sitestyle == 3) { |
| 3288 | uiViewMode = 'default3'; |
| 3289 | } |
| 3290 | // Refresh the session |
| 3291 | render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({ |
| 3292 | authCookie: authCookie, |
| 3293 | authRelayCookie: authRelayCookie, |
| 3294 | viewmode: viewmode, |
| 3295 | currentNode: currentNode, |
| 3296 | logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'), |
| 3297 | domain: domain.id, |
| 3298 | debuglevel: parent.debugLevel, |
| 3299 | serverDnsName: obj.getWebServerName(domain, req), |
| 3300 | serverRedirPort: args.redirport, |
| 3301 | serverPublicPort: httpsPort, |
| 3302 | serverfeatures: serverFeatures, |
| 3303 | features: allFeatures.features, |
| 3304 | features2: allFeatures.features2, |
| 3305 | features3: allFeatures.features3, |
| 3306 | sessiontime: (args.sessiontime) ? args.sessiontime : 60, |
| 3307 | mpspass: args.mpspass, |
| 3308 | passRequirements: passRequirements, |
| 3309 | customui: customui, |
| 3310 | customFiles: customFiles, |
| 3311 | webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'), |
| 3312 | footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, { |
| 3313 | 'serverversion': obj.parent.currentVer, |
| 3314 | 'servername': obj.getWebServerName(domain, req), |
| 3315 | 'agentsessions': Object.keys(parent.webserver.wsagents).length, |
| 3316 | 'connectedusers': Object.keys(parent.webserver.wssessions).length, |
| 3317 | 'userssessions': Object.keys(parent.webserver.wssessions2).length, |
| 3318 | 'relaysessions': parent.webserver.relaySessionCount, |
| 3319 | 'relaycount': Object.keys(parent.webserver.wsrelays).length |
| 3320 | }), |
| 3321 | webstate: encodeURIComponent(webstate).replace(/'/g, '%27'), |
| 3322 | amtscanoptions: amtscanoptions, |
| 3323 | pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(), |
| 3324 | 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)), |
| 3325 | webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''), |
| 3326 | hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'), |
| 3327 | showNotesPanel: (domain.shownotespanel ? 'true' : 'false'), |
| 3328 | userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'), |
| 3329 | webrtcconfig: webRtcConfig, |
| 3330 | collapseGroups: (domain.collapsegroups ? 'true' : 'false') |
| 3331 | }, dbGetFunc.req, domain, uiViewMode), user); |
| 3332 | } |
| 3333 | xdbGetFunc.req = req; |
| 3334 | xdbGetFunc.res = res; |
| 3335 | xdbGetFunc.user = user; |
| 3336 | obj.db.Get('ws' + user._id, xdbGetFunc); |
| 3337 | } else { |
| 3338 | // Send back the login application |
| 3339 | // If this is a 2 factor auth request, look for a hardware key challenge. |
| 3340 | // Normal login 2 factor request |
| 3341 | if (req.session && (req.session.loginmode == 4)) { |
| 3342 | const sec = parent.decryptSessionData(req.session.e); |
| 3343 | if ((sec != null) && (typeof sec.tuserid == 'string')) { |
| 3344 | const user = obj.users[sec.tuserid]; |
| 3345 | if (user != null) { |
| 3346 | parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.'); |
| 3347 | getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); }); |
| 3348 | return; |
| 3349 | } |
| 3350 | } |
| 3351 | } |
| 3352 | // Password recovery 2 factor request |
| 3353 | if (req.session && (req.session.loginmode == 5) && (req.session.temail)) { |
| 3354 | obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) { |
| 3355 | if ((err != null) || (docs.length == 0)) { |
| 3356 | parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.'); |
| 3357 | req.session = null; |
| 3358 | res.redirect(domain.url + getQueryPortion(req)); // BAD*** |
| 3359 | } else { |
| 3360 | var user = obj.users[docs[0]._id]; |
| 3361 | if (user != null) { |
| 3362 | parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.'); |
| 3363 | getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); }); |
| 3364 | } else { |
| 3365 | parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.'); |
| 3366 | req.session = null; |
| 3367 | res.redirect(domain.url + getQueryPortion(req)); // BAD*** |
| 3368 | } |
| 3369 | } |
| 3370 | }); |
| 3371 | return; |
| 3372 | } |
| 3373 | handleRootRequestLogin(req, res, domain, '', passRequirements); |
| 3374 | } |
| 3375 | } |
| 3376 | |
| 3377 | // Return a list of server supported features for a given domain and user |
| 3378 | obj.getDomainUserFeatures = function (domain, user, req) { |
| 3379 | var features = 0; |
| 3380 | var features2 = 0; |
| 3381 | var features3 = 0; |
| 3382 | if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode |
| 3383 | if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode |
| 3384 | if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode |
| 3385 | if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode |
| 3386 | if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA |
| 3387 | if ((parent.config.settings.allowframing != null) || (domain.allowframing != null) || (parent.config.settings.allowedframingorigins != null) || (domain.allowedframingorigins != null)) { features += 0x00000020; } // Allow site within iframe |
| 3388 | if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites |
| 3389 | if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now) |
| 3390 | // 0x00000100 --> This feature flag is free for future use. |
| 3391 | if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true) |
| 3392 | if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA |
| 3393 | if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update) |
| 3394 | 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 |
| 3395 | if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy |
| 3396 | if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support |
| 3397 | if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features |
| 3398 | if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints |
| 3399 | if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support |
| 3400 | if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) { |
| 3401 | // Check if we can skip 2nd factor auth because of the source IP address |
| 3402 | var skip2factor = false; |
| 3403 | if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) { |
| 3404 | for (var i in domain.passwordrequirements.skip2factor) { |
| 3405 | if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; } |
| 3406 | } |
| 3407 | } |
| 3408 | if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth |
| 3409 | } |
| 3410 | 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. |
| 3411 | if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible |
| 3412 | if (domain.usernameisemail) { features += 0x00200000; } // Username is email address |
| 3413 | if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels |
| 3414 | if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed |
| 3415 | if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes |
| 3416 | if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported |
| 3417 | if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed |
| 3418 | if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled |
| 3419 | if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature |
| 3420 | if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC |
| 3421 | if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js |
| 3422 | if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate |
| 3423 | if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active |
| 3424 | if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging |
| 3425 | if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging |
| 3426 | if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled |
| 3427 | if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update |
| 3428 | if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server |
| 3429 | if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled |
| 3430 | if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || ((domain.passwordrequirements.logintokens.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => domain.passwordrequirements.logintokens.indexOf(key) >= 0)) )))) { features2 += 0x00000080; } // Indicates login tokens are allowed |
| 3431 | if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes. |
| 3432 | if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled |
| 3433 | if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature |
| 3434 | if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get |
| 3435 | if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set |
| 3436 | if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly |
| 3437 | if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active |
| 3438 | if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name |
| 3439 | if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups |
| 3440 | if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled |
| 3441 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled |
| 3442 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use |
| 3443 | if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode |
| 3444 | if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode |
| 3445 | if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server |
| 3446 | 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 |
| 3447 | 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 |
| 3448 | if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled |
| 3449 | if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed |
| 3450 | if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button |
| 3451 | if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too |
| 3452 | if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed |
| 3453 | if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown |
| 3454 | if (domain.sitestyle === 3) { features2 |= 0x80000000; } // Indicates that Modern UI is forced (siteStyle = 3) |
| 3455 | if ((typeof domain.desktop == 'object') && (domain.desktop.disableconnectall == true)) { features3 += 0x00000001; } // Disable "Connect All" button when multiple sessions are active on a device |
| 3456 | if (domain.upninsteadofuser === true) { features3 += 0x00000002; } // Show UPN instead of username in General tab |
| 3457 | return { features: features, features2: features2, features3: features3 }; |
| 3458 | } |
| 3459 | |
| 3460 | function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) { |
| 3461 | parent.debug('web', 'handleRootRequestLogin()'); |
| 3462 | var features = 0; |
| 3463 | if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string') || (parent.config.settings.allowedframingorigins != null) || (domain != null && domain.allowedframingorigins != null))) { features += 32; } // Allow site within iframe |
| 3464 | if (domain.usernameisemail) { features += 0x00200000; } // Username is email address |
| 3465 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 3466 | var loginmode = 0; |
| 3467 | 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. |
| 3468 | |
| 3469 | // Format an error message if needed |
| 3470 | var passhint = null, msgid = 0; |
| 3471 | if (req.session != null) { |
| 3472 | msgid = req.session.messageid; |
| 3473 | if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); } |
| 3474 | delete req.session.messageid; |
| 3475 | delete req.session.passhint; |
| 3476 | } |
| 3477 | const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false)); |
| 3478 | 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')) |
| 3479 | |
| 3480 | // Check if we are allowed to create new users using the login screen |
| 3481 | var newAccountsAllowed = true; |
| 3482 | if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } } |
| 3483 | if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; } |
| 3484 | |
| 3485 | // Encrypt the hardware key challenge state if needed |
| 3486 | var hwstate = null; |
| 3487 | if (hardwareKeyChallenge && req.session) { |
| 3488 | const sec = parent.decryptSessionData(req.session.e); |
| 3489 | hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey) |
| 3490 | } |
| 3491 | |
| 3492 | // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5). |
| 3493 | var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string')); |
| 3494 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } |
| 3495 | var otpduo = (req.session != null) && (req.session.tduo === 1); |
| 3496 | if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; } |
| 3497 | var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1); |
| 3498 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } |
| 3499 | var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1); |
| 3500 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; } |
| 3501 | var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1); |
| 3502 | if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; } |
| 3503 | const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it. |
| 3504 | |
| 3505 | // See if we support two-factor trusted cookies |
| 3506 | var twoFactorCookieDays = 30; |
| 3507 | if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; } |
| 3508 | |
| 3509 | // See what authentication strategies we have |
| 3510 | var authStrategies = []; |
| 3511 | if (typeof domain.authstrategies == 'object') { |
| 3512 | if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); } |
| 3513 | if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); } |
| 3514 | if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); } |
| 3515 | if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); } |
| 3516 | if (typeof domain.authstrategies.oidc == 'object') { |
| 3517 | if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) { |
| 3518 | authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset); |
| 3519 | } else { |
| 3520 | authStrategies.push('oidc'); |
| 3521 | } |
| 3522 | } |
| 3523 | if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); } |
| 3524 | if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); } |
| 3525 | if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); } |
| 3526 | } |
| 3527 | |
| 3528 | // Custom user interface |
| 3529 | var customui = ''; |
| 3530 | if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); } |
| 3531 | |
| 3532 | // Custom files (CSS and JS) |
| 3533 | var customFiles = ''; |
| 3534 | if (domain.customFiles != null) { |
| 3535 | customFiles = encodeURIComponent(JSON.stringify(domain.customFiles)); |
| 3536 | } else if (domain.customfiles != null) { |
| 3537 | customFiles = encodeURIComponent(JSON.stringify(domain.customfiles)); |
| 3538 | } |
| 3539 | |
| 3540 | // Get two-factor screen timeout |
| 3541 | var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout. |
| 3542 | if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) { |
| 3543 | twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000; |
| 3544 | } |
| 3545 | |
| 3546 | // Setup CAPTCHA if needed |
| 3547 | var newAccountCaptcha = '', newAccountCaptchaImage = ''; |
| 3548 | if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) { |
| 3549 | newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey); |
| 3550 | newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha; |
| 3551 | } |
| 3552 | |
| 3553 | // Check for flash errors from passport.js and make the array unique |
| 3554 | var flashErrors = []; |
| 3555 | if (req.session.flash && req.session.flash.error) { |
| 3556 | flashErrors = obj.common.uniqueArray(req.session.flash.error); |
| 3557 | req.session.flash = null; |
| 3558 | } |
| 3559 | |
| 3560 | // Render the login page |
| 3561 | // Allow configurable OIDC login button text via domain.authstrategies.oidc.custom |
| 3562 | var oidcButtonIcon, oidcButtonIcon2x, oidcButtonText; |
| 3563 | if (obj.common.validateObject(domain.authstrategies) && obj.common.validateObject(domain.authstrategies.oidc) && obj.common.validateObject(domain.authstrategies.oidc.custom)) { |
| 3564 | if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl)) { |
| 3565 | oidcButtonIcon = domain.authstrategies.oidc.custom.buttoniconurl; |
| 3566 | if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl2x)) { |
| 3567 | oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl2x + ' 2x'; |
| 3568 | } else { |
| 3569 | oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl + ' 2x'; |
| 3570 | } |
| 3571 | } else { |
| 3572 | switch (domain.authstrategies.oidc.custom.preset) { |
| 3573 | case 'azure': |
| 3574 | oidcButtonIcon = "images/login/azure32.png"; |
| 3575 | oidcButtonIcon2x = "images/login/azure64.png 2x"; |
| 3576 | break; |
| 3577 | case 'google': |
| 3578 | oidcButtonIcon = "images/login/google32.png"; |
| 3579 | oidcButtonIcon2x = "images/login/google64.png 2x"; |
| 3580 | break; |
| 3581 | default: |
| 3582 | oidcButtonIcon = "images/login/oidc32.png"; |
| 3583 | oidcButtonIcon2x = "images/login/oidc64.png 2x"; |
| 3584 | } |
| 3585 | } |
| 3586 | |
| 3587 | if (obj.common.validateString(domain.authstrategies.oidc.custom.buttontext, 1, 128)) { |
| 3588 | oidcButtonText = domain.authstrategies.oidc.custom.buttontext; |
| 3589 | } |
| 3590 | } |
| 3591 | render(req, res, |
| 3592 | getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain), |
| 3593 | getRenderArgs({ |
| 3594 | loginmode: loginmode, |
| 3595 | rootCertLink: getRootCertLink(domain), |
| 3596 | newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page |
| 3597 | newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password |
| 3598 | newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty |
| 3599 | newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image |
| 3600 | serverDnsName: obj.getWebServerName(domain, req), |
| 3601 | serverPublicPort: httpsPort, |
| 3602 | passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true, |
| 3603 | emailcheck: emailcheck, |
| 3604 | features: features, |
| 3605 | sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default |
| 3606 | passRequirements: passRequirements, |
| 3607 | customui: customui, |
| 3608 | customFiles: customFiles, |
| 3609 | footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, { |
| 3610 | 'serverversion': obj.parent.currentVer, |
| 3611 | 'servername': obj.getWebServerName(domain, req), |
| 3612 | 'agentsessions': Object.keys(parent.webserver.wsagents).length, |
| 3613 | 'connectedusers': Object.keys(parent.webserver.wssessions).length, |
| 3614 | 'userssessions': Object.keys(parent.webserver.wssessions2).length, |
| 3615 | 'relaysessions': parent.webserver.relaySessionCount, |
| 3616 | 'relaycount': Object.keys(parent.webserver.wsrelays).length |
| 3617 | }), |
| 3618 | hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'), |
| 3619 | messageid: msgid, |
| 3620 | flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'), |
| 3621 | passhint: passhint, |
| 3622 | |
| 3623 | welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, { |
| 3624 | 'serverversion': obj.parent.currentVer, |
| 3625 | 'servername': obj.getWebServerName(domain, req), |
| 3626 | 'agentsessions': Object.keys(parent.webserver.wsagents).length, |
| 3627 | 'connectedusers': Object.keys(parent.webserver.wssessions).length, |
| 3628 | 'userssessions': Object.keys(parent.webserver.wssessions2).length, |
| 3629 | 'relaysessions': parent.webserver.relaySessionCount, |
| 3630 | 'relaycount': Object.keys(parent.webserver.wsrelays).length |
| 3631 | })).split('\'').join('\\\'') : null, |
| 3632 | welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false), |
| 3633 | hwstate: hwstate, |
| 3634 | otpemail: otpemail, |
| 3635 | otpduo: otpduo, |
| 3636 | otpsms: otpsms, |
| 3637 | otpmsg: otpmsg, |
| 3638 | otppush: otppush, |
| 3639 | autofido: autofido, |
| 3640 | twoFactorCookieDays: twoFactorCookieDays, |
| 3641 | authStrategies: authStrategies.join(','), |
| 3642 | oidcButtonText: oidcButtonText || '', |
| 3643 | oidcButtonIcon: oidcButtonIcon || 'images/login/oidc32.png', |
| 3644 | oidcButtonIcon2x: oidcButtonIcon2x || 'images/login/oidc64.png 2x', |
| 3645 | loginpicture: (typeof domain.loginpicture == 'string'), |
| 3646 | tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds, |
| 3647 | renderLanguages: obj.renderLanguages, |
| 3648 | showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false, |
| 3649 | }, req, domain, (domain.sitestyle >= 2) ? 'login2' : 'login')); |
| 3650 | } |
| 3651 | |
| 3652 | // Handle a post request on the root |
| 3653 | function handleRootPostRequest(req, res) { |
| 3654 | const domain = checkUserIpAddress(req, res); |
| 3655 | if (domain == null) { return; } |
| 3656 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key |
| 3657 | if (req.body == null) { req.body = {}; } |
| 3658 | parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action); |
| 3659 | |
| 3660 | // If a HTTP header is required, check new UserRequiredHttpHeader |
| 3661 | 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; } } |
| 3662 | |
| 3663 | switch (req.body.action) { |
| 3664 | case 'login': { handleLoginRequest(req, res, true); break; } |
| 3665 | case 'tokenlogin': { |
| 3666 | if (req.body.hwstate) { |
| 3667 | var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10); |
| 3668 | if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); } |
| 3669 | } |
| 3670 | handleLoginRequest(req, res, true); break; |
| 3671 | } |
| 3672 | case 'pushlogin': { |
| 3673 | if (req.body.hwstate) { |
| 3674 | var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1); |
| 3675 | if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) { |
| 3676 | // Push authentication is a success, login the user |
| 3677 | req.session = { userid: cookie.u }; |
| 3678 | |
| 3679 | // Check if we need to remember this device |
| 3680 | if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) { |
| 3681 | var maxCookieAge = domain.twofactorcookiedurationdays; |
| 3682 | if (typeof maxCookieAge != 'number') { maxCookieAge = 30; } |
| 3683 | const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey); |
| 3684 | res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true }); |
| 3685 | } |
| 3686 | var user = obj.users[cookie.u]; |
| 3687 | // Notify account login |
| 3688 | var targets = ['*', 'server-users', user._id]; |
| 3689 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 3690 | const ua = obj.getUserAgentInfo(req); |
| 3691 | const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'pushlogin' }; |
| 3692 | obj.parent.DispatchEvent(targets, obj, loginEvent); |
| 3693 | handleRootRequestEx(req, res, domain); |
| 3694 | return; |
| 3695 | } |
| 3696 | } |
| 3697 | handleLoginRequest(req, res, true); break; |
| 3698 | } |
| 3699 | case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; } |
| 3700 | case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; } |
| 3701 | case 'createaccount': { handleCreateAccountRequest(req, res, true); break; } |
| 3702 | case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; } |
| 3703 | case 'resetaccount': { handleResetAccountRequest(req, res, true); break; } |
| 3704 | case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; } |
| 3705 | default: { handleLoginRequest(req, res, true); break; } |
| 3706 | } |
| 3707 | } |
| 3708 | |
| 3709 | // Return true if it looks like we are using a real TLS certificate. |
| 3710 | obj.isTrustedCert = function (domain) { |
| 3711 | if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that. |
| 3712 | if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that. |
| 3713 | if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used. |
| 3714 | 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. |
| 3715 | if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert. |
| 3716 | if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name |
| 3717 | return true; // This is a guess |
| 3718 | } |
| 3719 | |
| 3720 | // Get the link to the root certificate if needed |
| 3721 | function getRootCertLink(domain) { |
| 3722 | // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate. |
| 3723 | if (obj.isTrustedCert(domain) == false) { |
| 3724 | // Get the domain suffix |
| 3725 | var xdomain = (domain.dns == null) ? domain.id : ''; |
| 3726 | if (xdomain != '') xdomain += '/'; |
| 3727 | return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>'; |
| 3728 | } |
| 3729 | return ''; |
| 3730 | } |
| 3731 | |
| 3732 | // Serve the xterm page |
| 3733 | function handleXTermRequest(req, res) { |
| 3734 | const domain = checkUserIpAddress(req, res); |
| 3735 | if (domain == null) { return; } |
| 3736 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 3737 | |
| 3738 | parent.debug('web', 'handleXTermRequest: sending xterm'); |
| 3739 | res.set({ 'Cache-Control': 'no-store' }); |
| 3740 | if (req.session && req.session.userid) { |
| 3741 | if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain |
| 3742 | var user = obj.users[req.session.userid]; |
| 3743 | if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists |
| 3744 | |
| 3745 | // Check permissions |
| 3746 | obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) { |
| 3747 | if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; } |
| 3748 | |
| 3749 | var logoutcontrols = { name: user.name }; |
| 3750 | var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; |
| 3751 | 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 |
| 3752 | |
| 3753 | // Create a authentication cookie |
| 3754 | const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey); |
| 3755 | const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey); |
| 3756 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 3757 | 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)); |
| 3758 | }); |
| 3759 | } else { |
| 3760 | res.redirect(domain.url + getQueryPortion(req)); |
| 3761 | return; |
| 3762 | } |
| 3763 | } |
| 3764 | |
| 3765 | // Handle new account Captcha GET |
| 3766 | function handleNewAccountCaptchaRequest(req, res) { |
| 3767 | const domain = checkUserIpAddress(req, res); |
| 3768 | if (domain == null) { return; } |
| 3769 | if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; } |
| 3770 | const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey); |
| 3771 | if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; } |
| 3772 | res.type('svg'); |
| 3773 | res.status(200).end(require('svg-captcha')(c.captcha, {})); |
| 3774 | } |
| 3775 | |
| 3776 | // Handle Captcha GET |
| 3777 | function handleCaptchaGetRequest(req, res) { |
| 3778 | const domain = checkUserIpAddress(req, res); |
| 3779 | if (domain == null) { return; } |
| 3780 | if (parent.crowdSecBounser == null) { res.sendStatus(404); return; } |
| 3781 | parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); }); |
| 3782 | } |
| 3783 | |
| 3784 | // Handle Captcha POST |
| 3785 | function handleCaptchaPostRequest(req, res) { |
| 3786 | if (parent.crowdSecBounser == null) { res.sendStatus(404); return; } |
| 3787 | const domain = checkUserIpAddress(req, res); |
| 3788 | if (domain == null) { return; } |
| 3789 | req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id)); |
| 3790 | parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); }); |
| 3791 | } |
| 3792 | |
| 3793 | // Render the terms of service. |
| 3794 | function handleTermsRequest(req, res) { |
| 3795 | const domain = checkUserIpAddress(req, res); |
| 3796 | if (domain == null) { return; } |
| 3797 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 3798 | |
| 3799 | // See if term.txt was loaded from the database |
| 3800 | if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) { |
| 3801 | // Send the terms from the database |
| 3802 | res.set({ 'Cache-Control': 'no-store' }); |
| 3803 | if (req.session && req.session.userid) { |
| 3804 | 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 |
| 3805 | var user = obj.users[req.session.userid]; |
| 3806 | var logoutcontrols = { name: user.name }; |
| 3807 | var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; |
| 3808 | 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 |
| 3809 | 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)); |
| 3810 | } else { |
| 3811 | render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain)); |
| 3812 | } |
| 3813 | } else { |
| 3814 | // See if there is a terms.txt file in meshcentral-data |
| 3815 | var p = obj.path.join(obj.parent.datapath, 'terms.txt'); |
| 3816 | if (obj.fs.existsSync(p)) { |
| 3817 | obj.fs.readFile(p, 'utf8', function (err, data) { |
| 3818 | if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; } |
| 3819 | |
| 3820 | // Send the terms from terms.txt |
| 3821 | res.set({ 'Cache-Control': 'no-store' }); |
| 3822 | if (req.session && req.session.userid) { |
| 3823 | 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 |
| 3824 | var user = obj.users[req.session.userid]; |
| 3825 | var logoutcontrols = { name: user.name }; |
| 3826 | var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; |
| 3827 | 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 |
| 3828 | render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain)); |
| 3829 | } else { |
| 3830 | render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain)); |
| 3831 | } |
| 3832 | }); |
| 3833 | } else { |
| 3834 | // Send the default terms |
| 3835 | parent.debug('web', 'handleTermsRequest: sending default terms'); |
| 3836 | res.set({ 'Cache-Control': 'no-store' }); |
| 3837 | if (req.session && req.session.userid) { |
| 3838 | 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 |
| 3839 | var user = obj.users[req.session.userid]; |
| 3840 | var logoutcontrols = { name: user.name }; |
| 3841 | var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''; |
| 3842 | 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 |
| 3843 | render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain)); |
| 3844 | } else { |
| 3845 | render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain)); |
| 3846 | } |
| 3847 | } |
| 3848 | } |
| 3849 | } |
| 3850 | |
| 3851 | // Render the messenger application. |
| 3852 | function handleMessengerRequest(req, res) { |
| 3853 | const domain = getDomain(req); |
| 3854 | if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; } |
| 3855 | parent.debug('web', 'handleMessengerRequest()'); |
| 3856 | |
| 3857 | // Check if we are in maintenance mode |
| 3858 | if (parent.config.settings.maintenancemode != null) { |
| 3859 | 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)); |
| 3860 | return; |
| 3861 | } |
| 3862 | |
| 3863 | // Check if this session is for a user |
| 3864 | if (req.query.id == null) { res.sendStatus(404); return; } |
| 3865 | var idSplit = decodeURIComponent(req.query.id).split('/'); |
| 3866 | if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; } |
| 3867 | if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) { |
| 3868 | // This is a user to user conversation, both users must be logged in. |
| 3869 | var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3] |
| 3870 | var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6] |
| 3871 | if (!req.session || !req.session.userid) { |
| 3872 | // Redirect to login page |
| 3873 | if (req.query.key != null) { res.redirect(domain.url + '?key=' + encodeURIComponent(req.query.key) + '&meshmessengerid=' + encodeURIComponent(req.query.id)); } else { res.redirect(domain.url + '?meshmessengerid=' + encodeURIComponent(req.query.id)); } |
| 3874 | return; |
| 3875 | } |
| 3876 | if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; } |
| 3877 | } |
| 3878 | |
| 3879 | // Get WebRTC configuration |
| 3880 | var webRtcConfig = null; |
| 3881 | if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); } |
| 3882 | else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); } |
| 3883 | |
| 3884 | // Setup other options |
| 3885 | var options = { webrtcconfig: webRtcConfig }; |
| 3886 | if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; } |
| 3887 | |
| 3888 | // Get the userid and name |
| 3889 | if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) { |
| 3890 | if (idSplit.length == 7) { |
| 3891 | const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]]; |
| 3892 | if (user != null) { |
| 3893 | if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); } |
| 3894 | if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); } |
| 3895 | } |
| 3896 | } |
| 3897 | } |
| 3898 | |
| 3899 | // Render the page |
| 3900 | res.set({ 'Cache-Control': 'no-store' }); |
| 3901 | render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain)); |
| 3902 | } |
| 3903 | |
| 3904 | // Handle messenger image request |
| 3905 | function handleMessengerImageRequest(req, res) { |
| 3906 | const domain = getDomain(req); |
| 3907 | if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; } |
| 3908 | parent.debug('web', 'handleMessengerImageRequest()'); |
| 3909 | |
| 3910 | // Check if we are in maintenance mode |
| 3911 | if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; } |
| 3912 | |
| 3913 | //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day |
| 3914 | if (domain.meshmessengerpicture) { |
| 3915 | // Use the configured messenger logo picture |
| 3916 | try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { } |
| 3917 | } |
| 3918 | |
| 3919 | var imagefile = 'images/messenger.png'; |
| 3920 | if (domain.webpublicpath != null) { |
| 3921 | obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) { |
| 3922 | if (exists) { |
| 3923 | // Use the domain logo picture |
| 3924 | try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 3925 | } else { |
| 3926 | // Use the default logo picture |
| 3927 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 3928 | } |
| 3929 | }); |
| 3930 | } else if (parent.webPublicOverridePath) { |
| 3931 | obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) { |
| 3932 | if (exists) { |
| 3933 | // Use the override logo picture |
| 3934 | try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 3935 | } else { |
| 3936 | // Use the default logo picture |
| 3937 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 3938 | } |
| 3939 | }); |
| 3940 | } else { |
| 3941 | // Use the default logo picture |
| 3942 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 3943 | } |
| 3944 | } |
| 3945 | |
| 3946 | // Returns the server root certificate encoded in base64 |
| 3947 | function getRootCertBase64() { |
| 3948 | var rootcert = obj.certificates.root.cert; |
| 3949 | var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n'); |
| 3950 | if (i >= 0) { rootcert = rootcert.substring(i + 29); } |
| 3951 | i = rootcert.indexOf('-----END CERTIFICATE-----'); |
| 3952 | if (i >= 0) { rootcert = rootcert.substring(i, 0); } |
| 3953 | return Buffer.from(rootcert, 'base64').toString('base64'); |
| 3954 | } |
| 3955 | |
| 3956 | // Returns the mesh server root certificate |
| 3957 | function handleRootCertRequest(req, res) { |
| 3958 | const domain = getDomain(req); |
| 3959 | if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; } |
| 3960 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 3961 | if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only. |
| 3962 | parent.debug('web', 'handleRootCertRequest()'); |
| 3963 | setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer'); |
| 3964 | res.send(Buffer.from(getRootCertBase64(), 'base64')); |
| 3965 | } |
| 3966 | |
| 3967 | // Return a customised mainifest.json for PWA |
| 3968 | function handleManifestRequest(req, res){ |
| 3969 | const domain = checkUserIpAddress(req); |
| 3970 | if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; } |
| 3971 | parent.debug('web', 'handleManifestRequest()'); |
| 3972 | var manifest = { |
| 3973 | "name": (domain.title != null) ? domain.title : 'MeshCentral', |
| 3974 | "short_name": (domain.title != null) ? domain.title : 'MeshCentral', |
| 3975 | "description": "Open source web based, remote computer management.", |
| 3976 | "scope": ".", |
| 3977 | "start_url": "/", |
| 3978 | "display": "fullscreen", |
| 3979 | "orientation": "any", |
| 3980 | "theme_color": "#ffffff", |
| 3981 | "background_color": "#ffffff", |
| 3982 | "icons": [{ |
| 3983 | "src": "pwalogo.png", |
| 3984 | "sizes": "512x512", |
| 3985 | "type": "image/png" |
| 3986 | }] |
| 3987 | }; |
| 3988 | res.json(manifest); |
| 3989 | } |
| 3990 | |
| 3991 | // Handle user public file downloads |
| 3992 | function handleDownloadUserFiles(req, res) { |
| 3993 | const domain = checkUserIpAddress(req, res); |
| 3994 | if (domain == null) { return; } |
| 3995 | if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 3996 | |
| 3997 | if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; } |
| 3998 | var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = ''; |
| 3999 | if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns |
| 4000 | if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; } |
| 4001 | if (domain.id != '') { domainname = 'domain-' + domain.id; } |
| 4002 | var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public'); |
| 4003 | 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; } } |
| 4004 | |
| 4005 | var stat = null; |
| 4006 | try { stat = obj.fs.statSync(path); } catch (e) { } |
| 4007 | if ((stat != null) && ((stat.mode & 0x004000) == 0)) { |
| 4008 | if (req.query.download == 1) { |
| 4009 | setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin'); |
| 4010 | try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); } |
| 4011 | } else { |
| 4012 | 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)); |
| 4013 | } |
| 4014 | } else { |
| 4015 | render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain)); |
| 4016 | } |
| 4017 | } |
| 4018 | |
| 4019 | // Handle device file request |
| 4020 | function handleDeviceFile(req, res) { |
| 4021 | const domain = getDomain(req, res); |
| 4022 | if (domain == null) { return; } |
| 4023 | if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; } |
| 4024 | |
| 4025 | // Check the inbound desktop sharing cookie |
| 4026 | var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 4027 | if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; } |
| 4028 | |
| 4029 | // Check userid |
| 4030 | const user = obj.users[c.userid]; |
| 4031 | if ((c == user)) { res.sendStatus(404); return; } |
| 4032 | |
| 4033 | // If this cookie has restricted usages, check that it's allowed to perform downloads |
| 4034 | if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10 |
| 4035 | |
| 4036 | if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid. |
| 4037 | if (req.query.n == null) { res.sendStatus(404); return; } |
| 4038 | |
| 4039 | // Check if this user has permission to manage this computer |
| 4040 | obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) { |
| 4041 | if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device |
| 4042 | |
| 4043 | // All good, start the file transfer |
| 4044 | req.query.id = getRandomLowerCase(12); |
| 4045 | obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id); |
| 4046 | }); |
| 4047 | } |
| 4048 | |
| 4049 | // Handle download of a server file by an agent |
| 4050 | function handleAgentDownloadFile(req, res) { |
| 4051 | const domain = checkAgentIpAddress(req, res); |
| 4052 | if (domain == null) { return; } |
| 4053 | if (req.query.c == null) { res.sendStatus(404); return; } |
| 4054 | |
| 4055 | // Check the inbound desktop sharing cookie |
| 4056 | var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout |
| 4057 | 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; } |
| 4058 | |
| 4059 | // Send the file back |
| 4060 | try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); } |
| 4061 | } |
| 4062 | |
| 4063 | // Handle logo request |
| 4064 | function handleLogoRequest(req, res) { |
| 4065 | const domain = checkUserIpAddress(req, res); |
| 4066 | if (domain == null) { return; } |
| 4067 | |
| 4068 | //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day |
| 4069 | if (domain.titlepicture) { |
| 4070 | if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) { |
| 4071 | // Use the logo in the database |
| 4072 | res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' }); |
| 4073 | res.send(parent.configurationFiles[domain.titlepicture]); |
| 4074 | return; |
| 4075 | } else { |
| 4076 | // Use the logo on file |
| 4077 | try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { } |
| 4078 | } |
| 4079 | } |
| 4080 | |
| 4081 | if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) { |
| 4082 | // Use the domain logo picture |
| 4083 | try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); } |
| 4084 | } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) { |
| 4085 | // Use the override logo picture |
| 4086 | try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); } |
| 4087 | } else { |
| 4088 | // Use the default logo picture |
| 4089 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); } |
| 4090 | } |
| 4091 | } |
| 4092 | |
| 4093 | // Handle login logo request |
| 4094 | function handleLoginLogoRequest(req, res) { |
| 4095 | const domain = checkUserIpAddress(req, res); |
| 4096 | if (domain == null) { return; } |
| 4097 | |
| 4098 | //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day |
| 4099 | if (domain.loginpicture) { |
| 4100 | if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) { |
| 4101 | // Use the logo in the database |
| 4102 | res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' }); |
| 4103 | res.send(parent.configurationFiles[domain.loginpicture]); |
| 4104 | return; |
| 4105 | } else { |
| 4106 | // Use the logo on file |
| 4107 | try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); } |
| 4108 | } |
| 4109 | } else { |
| 4110 | res.sendStatus(404); |
| 4111 | } |
| 4112 | } |
| 4113 | |
| 4114 | // Handle PWA logo request |
| 4115 | function handlePWALogoRequest(req, res) { |
| 4116 | const domain = checkUserIpAddress(req, res); |
| 4117 | if (domain == null) { return; } |
| 4118 | |
| 4119 | //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day |
| 4120 | if (domain.pwalogo) { |
| 4121 | if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) { |
| 4122 | // Use the logo in the database |
| 4123 | res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' }); |
| 4124 | res.send(parent.configurationFiles[domain.pwalogo]); |
| 4125 | return; |
| 4126 | } else { |
| 4127 | // Use the logo on file |
| 4128 | try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { } |
| 4129 | } |
| 4130 | } |
| 4131 | |
| 4132 | if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) { |
| 4133 | // Use the domain logo picture |
| 4134 | try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); } |
| 4135 | } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) { |
| 4136 | // Use the override logo picture |
| 4137 | try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); } |
| 4138 | } else { |
| 4139 | // Use the default logo picture |
| 4140 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); } |
| 4141 | } |
| 4142 | } |
| 4143 | |
| 4144 | // Handle translation request |
| 4145 | function handleTranslationsRequest(req, res) { |
| 4146 | const domain = checkUserIpAddress(req, res); |
| 4147 | if (domain == null) { return; } |
| 4148 | //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key |
| 4149 | if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only. |
| 4150 | |
| 4151 | var user = null; |
| 4152 | if (obj.args.user != null) { |
| 4153 | // A default user is active |
| 4154 | user = obj.users['user/' + domain.id + '/' + obj.args.user]; |
| 4155 | if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; } |
| 4156 | } else { |
| 4157 | // Check if the user is logged and we have all required parameters |
| 4158 | if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; } |
| 4159 | |
| 4160 | // Get the current user |
| 4161 | user = obj.users[req.session.userid]; |
| 4162 | if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; } |
| 4163 | if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; } |
| 4164 | } |
| 4165 | |
| 4166 | var data = ''; |
| 4167 | req.setEncoding('utf8'); |
| 4168 | req.on('data', function (chunk) { data += chunk; }); |
| 4169 | req.on('end', function () { |
| 4170 | try { data = JSON.parse(data); } catch (ex) { data = null; } |
| 4171 | if (data == null) { res.sendStatus(404); return; } |
| 4172 | if (data.action == 'getTranslations') { |
| 4173 | if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) { |
| 4174 | // Return the translation file (JSON) |
| 4175 | try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); } |
| 4176 | } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { |
| 4177 | // Return the default translation file (JSON) |
| 4178 | try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); } |
| 4179 | } else { res.sendStatus(404); } |
| 4180 | } else if (data.action == 'setTranslations') { |
| 4181 | 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 })); } }); |
| 4182 | } else if (data.action == 'translateServer') { |
| 4183 | if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; } |
| 4184 | const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); |
| 4185 | if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; } |
| 4186 | var translateFile = obj.path.join(obj.parent.datapath, 'translate.json'); |
| 4187 | if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); } |
| 4188 | if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; } |
| 4189 | res.send(JSON.stringify({ response: 'ok' })); |
| 4190 | console.log('Started server translation...'); |
| 4191 | obj.pendingTranslation = true; |
| 4192 | var child = require('child_process').spawn(process.argv[0],['translate.js', 'translateall', translateFile], { timeout: 300000, cwd: obj.path.join(__dirname, 'translate') }); |
| 4193 | var stdout = '', stderr = ''; |
| 4194 | child.stdout.on('data', function(d) { stdout += d; }); |
| 4195 | child.stderr.on('data', function(d) { stderr += d; }); |
| 4196 | child.on('close', function(error) { |
| 4197 | delete obj.pendingTranslation; |
| 4198 | if (error) { console.log('Server translation error', error); } |
| 4199 | // console.log('stdout', stdout); |
| 4200 | if (stderr) { console.log('Server translation stderr', stderr); } |
| 4201 | //console.log('Server restart...'); // Perform a server restart |
| 4202 | //process.exit(0); |
| 4203 | console.log('Server translation completed.'); |
| 4204 | stdout = null, stderr = null; |
| 4205 | }); |
| 4206 | } else { |
| 4207 | // Unknown request |
| 4208 | res.sendStatus(404); |
| 4209 | } |
| 4210 | }); |
| 4211 | } |
| 4212 | |
| 4213 | // Handle welcome image request |
| 4214 | function handleWelcomeImageRequest(req, res) { |
| 4215 | const domain = checkUserIpAddress(req, res); |
| 4216 | if (domain == null) { return; } |
| 4217 | |
| 4218 | //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day |
| 4219 | if (domain.welcomepicture) { |
| 4220 | if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) { |
| 4221 | // Use the welcome image in the database |
| 4222 | res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' }); |
| 4223 | res.send(parent.configurationFiles[domain.welcomepicture]); |
| 4224 | return; |
| 4225 | } |
| 4226 | |
| 4227 | // Use the configured logo picture |
| 4228 | try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { } |
| 4229 | } |
| 4230 | |
| 4231 | var imagefile = 'images/mainwelcome.jpg'; |
| 4232 | if (domain.sitestyle >= 2) { imagefile = 'images/login/back.png'; } |
| 4233 | if (domain.webpublicpath != null) { |
| 4234 | obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) { |
| 4235 | if (exists) { |
| 4236 | // Use the domain logo picture |
| 4237 | try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 4238 | } else { |
| 4239 | // Use the default logo picture |
| 4240 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 4241 | } |
| 4242 | }); |
| 4243 | } else if (parent.webPublicOverridePath) { |
| 4244 | obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) { |
| 4245 | if (exists) { |
| 4246 | // Use the override logo picture |
| 4247 | try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 4248 | } else { |
| 4249 | // Use the default logo picture |
| 4250 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 4251 | } |
| 4252 | }); |
| 4253 | } else { |
| 4254 | // Use the default logo picture |
| 4255 | try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); } |
| 4256 | } |
| 4257 | } |
| 4258 | |
| 4259 | // Download a session recording |
| 4260 | function handleGetRecordings(req, res) { |
| 4261 | const domain = checkUserIpAddress(req, res); |
| 4262 | if (domain == null) return; |
| 4263 | |
| 4264 | // Check the query |
| 4265 | if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (!req.query.file.endsWith('.mcrec') && !req.query.file.endsWith('.txt'))) { res.sendStatus(401); return; } |
| 4266 | |
| 4267 | // Get the recording path |
| 4268 | var recordingsPath = null; |
| 4269 | if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; } |
| 4270 | if (recordingsPath == null) { res.sendStatus(401); return; } |
| 4271 | |
| 4272 | // Get the user and check user rights |
| 4273 | var authUserid = null; |
| 4274 | if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } |
| 4275 | if (authUserid == null) { res.sendStatus(401); return; } |
| 4276 | const user = obj.users[authUserid]; |
| 4277 | if (user == null) { res.sendStatus(401); return; } |
| 4278 | if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings |
| 4279 | |
| 4280 | // Send the recorded file |
| 4281 | setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec'); |
| 4282 | try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); } |
| 4283 | } |
| 4284 | |
| 4285 | // Stream a session recording |
| 4286 | function handleGetRecordingsWebSocket(ws, req) { |
| 4287 | var domain = checkAgentIpAddress(ws, req); |
| 4288 | 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; } |
| 4289 | |
| 4290 | // Check the query |
| 4291 | 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; } |
| 4292 | |
| 4293 | // Get the recording path |
| 4294 | var recordingsPath = null; |
| 4295 | if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; } |
| 4296 | if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; } |
| 4297 | |
| 4298 | // Get the user and check user rights |
| 4299 | var authUserid = null; |
| 4300 | if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } |
| 4301 | if (authUserid == null) { try { ws.close(); } catch (ex) { } return; } |
| 4302 | const user = obj.users[authUserid]; |
| 4303 | if (user == null) { try { ws.close(); } catch (ex) { } return; } |
| 4304 | if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings |
| 4305 | const filefullpath = obj.path.join(recordingsPath, req.query.file); |
| 4306 | |
| 4307 | obj.fs.stat(filefullpath, function (err, stats) { |
| 4308 | if (err) { |
| 4309 | try { ws.close(); } catch (ex) { } // File does not exist |
| 4310 | } else { |
| 4311 | obj.fs.open(filefullpath, 'r', function (err, fd) { |
| 4312 | if (err == null) { |
| 4313 | // When data is received from the web socket |
| 4314 | ws.on('message', function (msg) { |
| 4315 | if (typeof msg != 'string') return; |
| 4316 | var command; |
| 4317 | try { command = JSON.parse(msg); } catch (e) { return; } |
| 4318 | if ((command == null) || (typeof command.action != 'string')) return; |
| 4319 | switch (command.action) { |
| 4320 | case 'get': { |
| 4321 | const buffer = Buffer.alloc(8 + command.size); |
| 4322 | //buffer.writeUInt32BE((command.ptr >> 32), 0); |
| 4323 | buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4); |
| 4324 | 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); }); |
| 4325 | break; |
| 4326 | } |
| 4327 | } |
| 4328 | }); |
| 4329 | |
| 4330 | // If error, do nothing |
| 4331 | ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); }); |
| 4332 | |
| 4333 | // If the web socket is closed |
| 4334 | ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); }); |
| 4335 | |
| 4336 | ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size })); |
| 4337 | } else { |
| 4338 | try { ws.close(); } catch (ex) { } |
| 4339 | } |
| 4340 | }); |
| 4341 | } |
| 4342 | }); |
| 4343 | } |
| 4344 | |
| 4345 | // Serve the player page |
| 4346 | function handlePlayerRequest(req, res) { |
| 4347 | const domain = checkUserIpAddress(req, res); |
| 4348 | if (domain == null) { return; } |
| 4349 | |
| 4350 | parent.debug('web', 'handlePlayerRequest: sending player'); |
| 4351 | res.set({ 'Cache-Control': 'no-store' }); |
| 4352 | render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain)); |
| 4353 | } |
| 4354 | |
| 4355 | // Serve the guest sharing page |
| 4356 | function handleSharingRequest(req, res) { |
| 4357 | const domain = getDomain(req, res); |
| 4358 | if (domain == null) { return; } |
| 4359 | if (req.query.c == null) { res.sendStatus(404); return; } |
| 4360 | if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed. |
| 4361 | |
| 4362 | // Check the inbound guest sharing cookie |
| 4363 | var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time. |
| 4364 | if (c == null) { res.sendStatus(404); return; } |
| 4365 | |
| 4366 | if (c.a === 5) { |
| 4367 | // This is the older style sharing cookie with everything encoded within it. |
| 4368 | // This cookie style gives a very large URL, so it's not used anymore. |
| 4369 | 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; } |
| 4370 | handleSharingRequestEx(req, res, domain, c); |
| 4371 | return; |
| 4372 | } |
| 4373 | if (c.a === 6) { |
| 4374 | // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database. |
| 4375 | // Gives a much more compact URL. |
| 4376 | if (typeof c.pid != 'string') { res.sendStatus(404); return; } |
| 4377 | |
| 4378 | // Check the expired time, expire message. |
| 4379 | if ((c.e != null) && (c.e <= Date.now())) { res.status(404); 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; } |
| 4380 | |
| 4381 | obj.db.Get('deviceshare-' + c.pid, function (err, docs) { |
| 4382 | if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; } |
| 4383 | const doc = docs[0]; |
| 4384 | |
| 4385 | // If this is a recurrent share, check if we are at the correct time to make use of it |
| 4386 | if (typeof doc.recurring == 'number') { |
| 4387 | const now = Date.now(); |
| 4388 | if (now >= doc.startTime) { // We don't want to move the validity window before the start time |
| 4389 | const deltaTime = (now - doc.startTime); |
| 4390 | if (doc.recurring === 1) { |
| 4391 | // This moves the start time to the next valid daily window |
| 4392 | const oneDay = (24 * 60 * 60 * 1000); |
| 4393 | var addition = Math.floor(deltaTime / oneDay); |
| 4394 | 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. |
| 4395 | doc.startTime += (addition * oneDay); |
| 4396 | } else if (doc.recurring === 2) { |
| 4397 | // This moves the start time to the next valid weekly window |
| 4398 | const oneWeek = (7 * 24 * 60 * 60 * 1000); |
| 4399 | var addition = Math.floor(deltaTime / oneWeek); |
| 4400 | 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. |
| 4401 | doc.startTime += (addition * oneWeek); |
| 4402 | } |
| 4403 | } |
| 4404 | } |
| 4405 | |
| 4406 | // Generate an old style cookie from the information in the database |
| 4407 | 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 }; |
| 4408 | if (doc.userid) { cookie.uid = doc.userid; } |
| 4409 | if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; } |
| 4410 | if (doc.startTime != null) { |
| 4411 | if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; } |
| 4412 | else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); } |
| 4413 | } |
| 4414 | if (doc.viewOnly === true) { cookie.vo = 1; } |
| 4415 | handleSharingRequestEx(req, res, domain, cookie); |
| 4416 | }); |
| 4417 | return; |
| 4418 | } |
| 4419 | res.sendStatus(404); return; |
| 4420 | } |
| 4421 | |
| 4422 | // Serve the guest sharing page |
| 4423 | function handleSharingRequestEx(req, res, domain, c) { |
| 4424 | // Check the expired time, expire message. |
| 4425 | if ((c.expire != null) && (c.expire <= Date.now())) { res.status(404); 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; } |
| 4426 | |
| 4427 | // Check the public id |
| 4428 | obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) { |
| 4429 | // Check if any sharing links are present, expire message. |
| 4430 | if ((err != null) || (docs.length == 0)) { res.status(404); 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; } |
| 4431 | |
| 4432 | // Search for the device share public identifier, expire message. |
| 4433 | var found = false; |
| 4434 | 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; } } |
| 4435 | if (found == false) { res.status(404); 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; } |
| 4436 | |
| 4437 | // Get information about this node |
| 4438 | obj.db.Get(c.nid, function (err, nodes) { |
| 4439 | if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; } |
| 4440 | var node = nodes[0]; |
| 4441 | |
| 4442 | // Check the start time, not yet valid message. |
| 4443 | if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { res.status(404); 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; } |
| 4444 | |
| 4445 | // If this is a web relay share, check if this feature is active |
| 4446 | if ((c.p == 8) || (c.p == 16)) { |
| 4447 | // This is a HTTP or HTTPS share |
| 4448 | 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)); |
| 4449 | if (webRelayPort == 0) { res.sendStatus(404); return; } |
| 4450 | |
| 4451 | // Create the authentication cookie |
| 4452 | 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 }; |
| 4453 | if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; } |
| 4454 | const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey); |
| 4455 | |
| 4456 | // Redirect to a URL |
| 4457 | var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req); |
| 4458 | var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie; |
| 4459 | if (c.addr != null) { url += '&addr=' + c.addr; } |
| 4460 | if (c.pid != null) { url += '&relayid=' + c.pid; } |
| 4461 | parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".'); |
| 4462 | res.redirect(url); |
| 4463 | } else { |
| 4464 | // Looks good, let's create the outbound session cookies. |
| 4465 | // This is a desktop, terminal or files share. We need to display the sharing page. |
| 4466 | // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar. |
| 4467 | 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 }; |
| 4468 | if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; } |
| 4469 | if (c.k != null) { authCookieData.k = c.k; } |
| 4470 | const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey); |
| 4471 | |
| 4472 | // Server features |
| 4473 | var features2 = 0; |
| 4474 | if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true) |
| 4475 | |
| 4476 | // Lets respond by sending out the desktop viewer. |
| 4477 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 4478 | parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".'); |
| 4479 | res.set({ 'Cache-Control': 'no-store' }); |
| 4480 | 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)); |
| 4481 | } |
| 4482 | }); |
| 4483 | }); |
| 4484 | } |
| 4485 | |
| 4486 | // Handle domain redirection |
| 4487 | obj.handleDomainRedirect = function (req, res) { |
| 4488 | const domain = checkUserIpAddress(req, res); |
| 4489 | if (domain == null) { return; } |
| 4490 | if (domain.redirects == null) { res.sendStatus(404); return; } |
| 4491 | var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?'); |
| 4492 | if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; } |
| 4493 | if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); } |
| 4494 | if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; } |
| 4495 | if (domain.redirects[urlName] == '~showversion') { |
| 4496 | // Show the current version |
| 4497 | res.end('MeshCentral v' + obj.parent.currentVer); |
| 4498 | } else { |
| 4499 | // Perform redirection |
| 4500 | res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req)); |
| 4501 | } |
| 4502 | } |
| 4503 | |
| 4504 | // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed |
| 4505 | obj.getServerFilePath = function (user, domain, path) { |
| 4506 | var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = ''; |
| 4507 | if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation |
| 4508 | var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2]; |
| 4509 | if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed |
| 4510 | if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights |
| 4511 | if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed |
| 4512 | serverpath += ('/' + splitpath[0] + '-' + splitpath[2]); |
| 4513 | 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 |
| 4514 | return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) }; |
| 4515 | }; |
| 4516 | |
| 4517 | // Return the maximum number of bytes allowed in the user account "My Files". |
| 4518 | obj.getQuota = function (objid, domain) { |
| 4519 | if (objid == null) return 0; |
| 4520 | if (objid.startsWith('user/')) { |
| 4521 | var user = obj.users[objid]; |
| 4522 | if (user == null) return 0; |
| 4523 | if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit |
| 4524 | if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; } |
| 4525 | if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; } |
| 4526 | return null; // By default, the user will have no limit |
| 4527 | } else if (objid.startsWith('mesh/')) { |
| 4528 | var mesh = obj.meshes[objid]; |
| 4529 | if (mesh == null) return 0; |
| 4530 | if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; } |
| 4531 | if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; } |
| 4532 | return null; // By default, the mesh will have no limit |
| 4533 | } |
| 4534 | return 0; |
| 4535 | }; |
| 4536 | |
| 4537 | // Download a file from the server |
| 4538 | function handleDownloadFile(req, res) { |
| 4539 | const domain = checkUserIpAddress(req, res); |
| 4540 | if (domain == null) { return; } |
| 4541 | if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; } |
| 4542 | const user = obj.users[req.session.userid]; |
| 4543 | if (user == null) { res.sendStatus(404); return; } |
| 4544 | const file = obj.getServerFilePath(user, domain, req.query.link); |
| 4545 | if (file == null) { res.sendStatus(404); return; } |
| 4546 | setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin'); |
| 4547 | obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } }); |
| 4548 | } |
| 4549 | |
| 4550 | // Download the MeshCommander web page |
| 4551 | function handleMeshCommander(req, res) { |
| 4552 | const domain = checkUserIpAddress(req, res); |
| 4553 | if (domain == null) { return; } |
| 4554 | if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; } |
| 4555 | |
| 4556 | // Find the correct MeshCommander language to send |
| 4557 | const acceptableLanguages = obj.getLanguageCodes(req); |
| 4558 | 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' }; |
| 4559 | for (var i in acceptableLanguages) { |
| 4560 | const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]]; |
| 4561 | if (meshCommanderLanguage != null) { |
| 4562 | try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { } |
| 4563 | return; |
| 4564 | } |
| 4565 | } |
| 4566 | |
| 4567 | // Send out the default english MeshCommander |
| 4568 | try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { } |
| 4569 | } |
| 4570 | |
| 4571 | // Upload a MeshCore.js file to the server |
| 4572 | function handleUploadMeshCoreFile(req, res) { |
| 4573 | const domain = checkUserIpAddress(req, res); |
| 4574 | if (domain == null) { return; } |
| 4575 | if (domain.id !== '') { res.sendStatus(401); return; } |
| 4576 | |
| 4577 | var authUserid = null; |
| 4578 | if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } |
| 4579 | |
| 4580 | const multiparty = require('multiparty'); |
| 4581 | const form = new multiparty.Form(); |
| 4582 | form.parse(req, function (err, fields, files) { |
| 4583 | // If an authentication cookie is embedded in the form, use that. |
| 4584 | if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { |
| 4585 | var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 4586 | if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. |
| 4587 | if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication |
| 4588 | } |
| 4589 | if (authUserid == null) { res.sendStatus(401); return; } |
| 4590 | if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; } |
| 4591 | |
| 4592 | // Get the user |
| 4593 | const user = obj.users[authUserid]; |
| 4594 | if (user == null) { res.sendStatus(401); return; } // Check this user exists |
| 4595 | |
| 4596 | // Get the node and check node rights |
| 4597 | const nodeid = fields.attrib[0]; |
| 4598 | obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) { |
| 4599 | if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device |
| 4600 | for (var i in files.files) { |
| 4601 | var file = files.files[i]; |
| 4602 | const uploadTempPath = resolveSafeUploadTempPath(file.path); |
| 4603 | if (uploadTempPath == null) { res.sendStatus(400); return; } |
| 4604 | obj.fs.readFile(uploadTempPath, 'utf8', function (err, data) { |
| 4605 | if (err != null) return; |
| 4606 | data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw) |
| 4607 | obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core |
| 4608 | try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { } |
| 4609 | }); |
| 4610 | } |
| 4611 | res.send(''); |
| 4612 | }); |
| 4613 | }); |
| 4614 | } |
| 4615 | |
| 4616 | // Upload a MeshCore.js file to the server |
| 4617 | function handleOneClickRecoveryFile(req, res) { |
| 4618 | const domain = checkUserIpAddress(req, res); |
| 4619 | if (domain == null) { return; } |
| 4620 | if (domain.id !== '') { res.sendStatus(401); return; } |
| 4621 | |
| 4622 | var authUserid = null; |
| 4623 | if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } |
| 4624 | |
| 4625 | const multiparty = require('multiparty'); |
| 4626 | const form = new multiparty.Form(); |
| 4627 | form.parse(req, function (err, fields, files) { |
| 4628 | // If an authentication cookie is embedded in the form, use that. |
| 4629 | if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { |
| 4630 | var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 4631 | if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. |
| 4632 | if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication |
| 4633 | } |
| 4634 | if (authUserid == null) { res.sendStatus(401); return; } |
| 4635 | if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; } |
| 4636 | |
| 4637 | // Get the user |
| 4638 | const user = obj.users[authUserid]; |
| 4639 | if (user == null) { res.sendStatus(401); return; } // Check this user exists |
| 4640 | |
| 4641 | // Get the node and check node rights |
| 4642 | const nodeid = fields.attrib[0]; |
| 4643 | obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) { |
| 4644 | if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device |
| 4645 | for (var i in files.files) { |
| 4646 | var file = files.files[i]; |
| 4647 | const uploadTempPath = resolveSafeUploadTempPath(file.path); |
| 4648 | if (uploadTempPath == null) { res.sendStatus(400); return; } |
| 4649 | |
| 4650 | // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers. |
| 4651 | parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: uploadTempPath }); |
| 4652 | |
| 4653 | //try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { } // TODO: Remove this file after 30 minutes. |
| 4654 | } |
| 4655 | res.send(''); |
| 4656 | }); |
| 4657 | }); |
| 4658 | } |
| 4659 | |
| 4660 | // Upload a file to the server |
| 4661 | function getCustomIconUserKey(user) { |
| 4662 | if ((user == null) || (typeof user._id !== 'string') || (user._id.length === 0)) { return null; } |
| 4663 | return obj.crypto.createHash('sha256').update(user._id).digest('hex'); |
| 4664 | } |
| 4665 | |
| 4666 | function getCustomIconUserDir(user) { |
| 4667 | const userKey = getCustomIconUserKey(user); |
| 4668 | if (userKey == null) { return null; } |
| 4669 | return obj.path.join(obj.parent.datapath, 'icons', 'custom', userKey); |
| 4670 | } |
| 4671 | |
| 4672 | // Maximum accepted custom icon upload size, in bytes. |
| 4673 | const customIconMaxFileSize = 10485760; |
| 4674 | // Maximum accepted width or height for uploaded PNG/JPEG sidebar icons, in pixels. |
| 4675 | const customIconMaxDimension = 64; |
| 4676 | // Image extensions accepted for uploaded custom sidebar icons. |
| 4677 | const customIconAllowedExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg']); |
| 4678 | |
| 4679 | /** |
| 4680 | * Return the HTTP response MIME type for a stored custom icon filename. |
| 4681 | * |
| 4682 | * @param {string} iconName Filename or path segment for the stored custom icon. |
| 4683 | * @returns {string|null} MIME type for supported icons, or null for unsupported extensions. |
| 4684 | */ |
| 4685 | function getCustomIconMimeType(iconName) { |
| 4686 | const lower = iconName.toLowerCase(); |
| 4687 | if (lower.endsWith('.svg')) { return 'image/svg+xml'; } |
| 4688 | if (lower.endsWith('.png')) { return 'image/png'; } |
| 4689 | if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) { return 'image/jpeg'; } |
| 4690 | return null; |
| 4691 | } |
| 4692 | |
| 4693 | /** |
| 4694 | * Reject SVG content that can execute script or load external content. |
| 4695 | * |
| 4696 | * @param {string} svgContent Raw UTF-8 SVG content from the uploaded file. |
| 4697 | * @returns {string|null} SVG content safe to store, or null if it is invalid or unsafe. |
| 4698 | */ |
| 4699 | function cleanSvg(svgContent) { |
| 4700 | if (typeof svgContent !== 'string') { return null; } |
| 4701 | const cleaned = (svgContent.charCodeAt(0) === 0xFEFF) ? svgContent.substring(1) : svgContent; |
| 4702 | if (cleaned.search(/<svg[\s>]/i) < 0) { return null; } |
| 4703 | if (cleaned.search(/<\s*(script|foreignObject|iframe|object|embed|applet|link|meta)\b/i) >= 0) { return null; } |
| 4704 | if (cleaned.search(/\s+on[a-z0-9_-]+\s*=/i) >= 0) { return null; } |
| 4705 | if (cleaned.search(/\s+(href|xlink:href|src)\s*=\s*(['"]?)\s*(?!#)/i) >= 0) { return null; } |
| 4706 | return cleaned; |
| 4707 | } |
| 4708 | |
| 4709 | /** |
| 4710 | * Check if a JPEG marker is a Start Of Frame marker that contains image dimensions. |
| 4711 | * |
| 4712 | * @param {number} marker JPEG marker byte after the 0xFF prefix. |
| 4713 | * @returns {boolean} True when the marker segment contains width and height fields. |
| 4714 | */ |
| 4715 | function isJpegStartOfFrameMarker(marker) { |
| 4716 | return ((marker >= 0xC0) && (marker <= 0xC3)) || ((marker >= 0xC5) && (marker <= 0xC7)) || ((marker >= 0xC9) && (marker <= 0xCB)) || ((marker >= 0xCD) && (marker <= 0xCF)); |
| 4717 | } |
| 4718 | |
| 4719 | /** |
| 4720 | * Read JPEG dimensions from header bytes without fully decoding the image. |
| 4721 | * |
| 4722 | * @param {Buffer} data Initial bytes from the uploaded JPEG file. |
| 4723 | * @returns {{width:number,height:number}|null} Parsed dimensions, or null if the JPEG header is invalid or incomplete. |
| 4724 | */ |
| 4725 | function getJpegDimensions(data) { |
| 4726 | // JPEG files must start with the SOI marker. |
| 4727 | if ((data.length < 4) || (data[0] !== 0xFF) || (data[1] !== 0xD8)) { return null; } |
| 4728 | // Start scanning after the SOI marker. |
| 4729 | var offset = 2; |
| 4730 | while (offset + 9 < data.length) { |
| 4731 | // Each JPEG segment starts with a marker prefix. |
| 4732 | if (data[offset] !== 0xFF) { return null; } |
| 4733 | // Skip fill bytes before the marker value. |
| 4734 | while ((offset < data.length) && (data[offset] === 0xFF)) { offset++; } |
| 4735 | const marker = data[offset++]; |
| 4736 | // SOI/EOI and restart markers do not carry segment lengths. |
| 4737 | if ((marker === 0xD8) || (marker === 0xD9)) { continue; } |
| 4738 | if ((marker >= 0xD0) && (marker <= 0xD7)) { continue; } |
| 4739 | // Remaining markers should include a two-byte segment length. |
| 4740 | if (offset + 2 > data.length) { return null; } |
| 4741 | const segmentLength = data.readUInt16BE(offset); |
| 4742 | if (segmentLength < 2) { return null; } |
| 4743 | if (isJpegStartOfFrameMarker(marker)) { |
| 4744 | // SOF payload layout: precision, height, width. |
| 4745 | if (offset + 7 > data.length) { return null; } |
| 4746 | return { width: data.readUInt16BE(offset + 5), height: data.readUInt16BE(offset + 3) }; |
| 4747 | } |
| 4748 | // Move to the next marker segment. |
| 4749 | offset += segmentLength; |
| 4750 | } |
| 4751 | return null; |
| 4752 | } |
| 4753 | |
| 4754 | /** |
| 4755 | * Validate the raster image signature and extract dimensions for supported custom icon formats. |
| 4756 | * |
| 4757 | * @param {Buffer} data Initial bytes from the uploaded icon file. |
| 4758 | * @param {string} extension Lowercase extension from the original uploaded filename. |
| 4759 | * @returns {{width:number,height:number}|null} Parsed raster dimensions, or null if the signature/type is invalid. |
| 4760 | */ |
| 4761 | function getCustomIconDimensions(data, extension) { |
| 4762 | // PNG dimensions are fixed in the IHDR chunk at byte offsets 16 and 20. |
| 4763 | if ((extension === '.png') && (data.length >= 24) && (data[0] === 0x89) && (data[1] === 0x50) && (data[2] === 0x4E) && (data[3] === 0x47) && (data[4] === 0x0D) && (data[5] === 0x0A) && (data[6] === 0x1A) && (data[7] === 0x0A)) { |
| 4764 | return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) }; |
| 4765 | } |
| 4766 | // JPEG dimensions are stored in the first SOF marker segment. |
| 4767 | if (((extension === '.jpg') || (extension === '.jpeg')) && (data.length >= 3) && (data[0] === 0xFF) && (data[1] === 0xD8) && (data[2] === 0xFF)) { |
| 4768 | return getJpegDimensions(data); |
| 4769 | } |
| 4770 | return null; |
| 4771 | } |
| 4772 | |
| 4773 | /** |
| 4774 | * Enforce custom icon upload policy before moving the temp file into persistent storage. |
| 4775 | * SVG files are checked for active content; PNG/JPEG files are signature and dimension checked. |
| 4776 | * |
| 4777 | * @param {string} iconTempPath Safe resolved path to the uploaded temp file. |
| 4778 | * @param {string} extension Lowercase extension from the original uploaded filename. |
| 4779 | * @param {function(string|null):void} callback Called with null on success, or a user-safe error message on failure. |
| 4780 | */ |
| 4781 | function validateCustomIconFile(iconTempPath, extension, callback) { |
| 4782 | obj.fs.stat(iconTempPath, function (statErr, stats) { |
| 4783 | if (statErr) { callback('Unable to read uploaded icon.'); return; } |
| 4784 | if ((stats == null) || (stats.isFile() !== true)) { callback('Invalid icon file.'); return; } |
| 4785 | // Reject empty and oversized uploads before reading any file content. |
| 4786 | if ((stats.size < 4) || (stats.size > customIconMaxFileSize)) { callback('Icon files must be non-empty and ' + (customIconMaxFileSize / 1048576) + ' MB or smaller.'); return; } |
| 4787 | if (extension === '.svg') { |
| 4788 | obj.fs.readFile(iconTempPath, 'utf8', function (readErr, svgContent) { |
| 4789 | if (readErr) { callback('Unable to read uploaded icon.'); return; } |
| 4790 | const cleanedSvg = cleanSvg(svgContent); |
| 4791 | if (cleanedSvg == null) { callback('Invalid SVG icon file.'); return; } |
| 4792 | obj.fs.writeFile(iconTempPath, cleanedSvg, 'utf8', function (writeErr) { |
| 4793 | callback(writeErr ? 'Unable to clean uploaded SVG icon.' : null); |
| 4794 | }); |
| 4795 | }); |
| 4796 | return; |
| 4797 | } |
| 4798 | obj.fs.open(iconTempPath, 'r', function (openErr, fd) { |
| 4799 | if (openErr) { callback('Unable to read uploaded icon.'); return; } |
| 4800 | // Reading the first 64 KB is enough for normal PNG headers and JPEG SOF markers. |
| 4801 | const header = Buffer.alloc(Math.min(stats.size, 65536)); |
| 4802 | obj.fs.read(fd, header, 0, header.length, 0, function (readErr, bytesRead) { |
| 4803 | obj.fs.close(fd, function () { }); |
| 4804 | if (readErr) { callback('Unable to read uploaded icon.'); return; } |
| 4805 | const dimensions = getCustomIconDimensions(header.slice(0, bytesRead), extension); |
| 4806 | if (dimensions == null) { callback('The uploaded icon does not match its file type.'); return; } |
| 4807 | if ((dimensions.width < 1) || (dimensions.height < 1) || (dimensions.width > customIconMaxDimension) || (dimensions.height > customIconMaxDimension)) { callback('Icon images must be ' + customIconMaxDimension + ' x ' + customIconMaxDimension + ' pixels or smaller.'); return; } |
| 4808 | callback(null); |
| 4809 | }); |
| 4810 | }); |
| 4811 | }); |
| 4812 | } |
| 4813 | |
| 4814 | function handleCustomIconUpload(req, res) { |
| 4815 | const domain = checkUserIpAddress(req, res); |
| 4816 | if (domain == null) { return; } |
| 4817 | if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; } |
| 4818 | const user = obj.users[req.session.userid]; |
| 4819 | if (user == null) { res.sendStatus(401); return; } |
| 4820 | |
| 4821 | const multiparty = require('multiparty'); |
| 4822 | const form = new multiparty.Form({ maxFilesSize: customIconMaxFileSize }); |
| 4823 | form.parse(req, function (err, fields, files) { |
| 4824 | if (err) { res.status(400).json({ success: false, error: (err.status === 413) ? 'Icon files must be non-empty and ' + (customIconMaxFileSize / 1048576) + ' MB or smaller.' : 'Invalid form submission.' }); return; } |
| 4825 | |
| 4826 | const allowedTypes = { myDevices: 1, myAccount: 1, myEvents: 1, myFiles: 1, myUsers: 1, myServer: 1 }; |
| 4827 | const iconType = (fields && fields.iconType && fields.iconType[0]) ? fields.iconType[0] : null; |
| 4828 | if ((typeof iconType !== 'string') || (allowedTypes[iconType] !== 1)) { res.status(400).json({ success: false, error: 'Invalid icon type.' }); return; } |
| 4829 | |
| 4830 | const iconFile = (files && files.iconFile && files.iconFile[0]) ? files.iconFile[0] : null; |
| 4831 | if ((iconFile == null) || (typeof iconFile.path !== 'string')) { res.status(400).json({ success: false, error: 'Missing icon file.' }); return; } |
| 4832 | const iconTempPath = resolveSafeUploadTempPath(iconFile.path); |
| 4833 | if (iconTempPath == null) { res.status(400).json({ success: false, error: 'Invalid icon file location.' }); return; } |
| 4834 | |
| 4835 | const cleanupTempFile = function () { try { obj.fs.unlink(iconTempPath, function () { }); } catch (ex) { } }; |
| 4836 | |
| 4837 | const extension = obj.path.extname(iconFile.originalFilename || '').toLowerCase(); |
| 4838 | if (customIconAllowedExtensions.has(extension) === false) { cleanupTempFile(); res.status(400).json({ success: false, error: 'Only SVG, PNG and JPEG icon files are supported.' }); return; } |
| 4839 | |
| 4840 | const iconsRoot = obj.path.join(obj.parent.datapath, 'icons'); |
| 4841 | const customDir = obj.path.join(iconsRoot, 'custom'); |
| 4842 | const userCustomDir = getCustomIconUserDir(user); |
| 4843 | const userKey = getCustomIconUserKey(user); |
| 4844 | if ((userCustomDir == null) || (userKey == null)) { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; } |
| 4845 | try { obj.fs.mkdirSync(iconsRoot); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } } |
| 4846 | try { obj.fs.mkdirSync(customDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } } |
| 4847 | try { obj.fs.mkdirSync(userCustomDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; } } |
| 4848 | |
| 4849 | const previousIcon = (fields && fields.previousIcon && fields.previousIcon[0]) ? fields.previousIcon[0] : null; |
| 4850 | const previousInfo = resolveCustomIconPath(previousIcon, user); |
| 4851 | |
| 4852 | const newFilename = iconType + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8) + extension; |
| 4853 | const destinationPath = obj.path.join(userCustomDir, newFilename); |
| 4854 | |
| 4855 | const respondSuccess = function () { |
| 4856 | if ((previousInfo != null) && (previousInfo.isOwned === true)) { |
| 4857 | try { obj.fs.unlinkSync(previousInfo.diskPath); } catch (ex) { } |
| 4858 | } |
| 4859 | res.json({ success: true, path: domain.url + 'icons/custom/' + userKey + '/' + newFilename }); |
| 4860 | }; |
| 4861 | |
| 4862 | validateCustomIconFile(iconTempPath, extension, function (validationError) { |
| 4863 | if (validationError != null) { cleanupTempFile(); res.status(400).json({ success: false, error: validationError }); return; } |
| 4864 | obj.fs.rename(iconTempPath, destinationPath, function (renameErr) { |
| 4865 | if (renameErr == null) { respondSuccess(); return; } |
| 4866 | if ((renameErr != null) && (renameErr.code === 'EXDEV')) { |
| 4867 | obj.common.copyFile(iconTempPath, destinationPath, function (copyErr) { |
| 4868 | cleanupTempFile(); |
| 4869 | if (copyErr) { res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' }); return; } |
| 4870 | respondSuccess(); |
| 4871 | }); |
| 4872 | } else { |
| 4873 | cleanupTempFile(); |
| 4874 | res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' }); |
| 4875 | } |
| 4876 | }); |
| 4877 | }); |
| 4878 | }); |
| 4879 | } |
| 4880 | |
| 4881 | function resolveCustomIconPath(requestPath, user) { |
| 4882 | if (typeof requestPath !== 'string') { return null; } |
| 4883 | if (requestPath.startsWith('http://') || requestPath.startsWith('https://') || requestPath.startsWith('data:')) { return null; } |
| 4884 | const pathOnly = requestPath.split('?')[0].split('#')[0]; |
| 4885 | const marker = '/icons/custom/'; |
| 4886 | const markerIndex = pathOnly.indexOf(marker); |
| 4887 | if (markerIndex < 0) { return null; } |
| 4888 | const relativePath = pathOnly.substring(markerIndex + marker.length); |
| 4889 | if ((relativePath.length === 0) || (relativePath.indexOf('\\') !== -1)) { return null; } |
| 4890 | const pathParts = relativePath.split('/'); |
| 4891 | if ((pathParts.length !== 1) && (pathParts.length !== 2)) { return null; } |
| 4892 | for (var i = 0; i < pathParts.length; i++) { |
| 4893 | if ((pathParts[i].length === 0) || (obj.common.IsFilenameValid(pathParts[i]) !== true)) { return null; } |
| 4894 | } |
| 4895 | |
| 4896 | var ownerKey = null, iconName = null, diskPath = null, isOwned = false; |
| 4897 | const iconsRoot = obj.path.join(obj.parent.datapath, 'icons', 'custom'); |
| 4898 | if (pathParts.length === 1) { |
| 4899 | iconName = pathParts[0]; |
| 4900 | diskPath = obj.path.join(iconsRoot, iconName); |
| 4901 | } else { |
| 4902 | ownerKey = pathParts[0]; |
| 4903 | iconName = pathParts[1]; |
| 4904 | diskPath = obj.path.join(iconsRoot, ownerKey, iconName); |
| 4905 | const currentUserKey = getCustomIconUserKey(user); |
| 4906 | isOwned = (currentUserKey != null) && (ownerKey === currentUserKey); |
| 4907 | } |
| 4908 | |
| 4909 | const lower = iconName.toLowerCase(); |
| 4910 | if ((lower.endsWith('.svg') === false) && (lower.endsWith('.png') === false) && (lower.endsWith('.jpg') === false) && (lower.endsWith('.jpeg') === false)) { return null; } |
| 4911 | return { ownerKey: ownerKey, iconName: iconName, diskPath: diskPath, isOwned: isOwned, isLegacy: (pathParts.length === 1) }; |
| 4912 | } |
| 4913 | |
| 4914 | function handleCustomIconDelete(req, res) { |
| 4915 | const domain = checkUserIpAddress(req, res); |
| 4916 | if (domain == null) { return; } |
| 4917 | if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; } |
| 4918 | const user = obj.users[req.session.userid]; |
| 4919 | if (user == null) { res.sendStatus(401); return; } |
| 4920 | |
| 4921 | const iconPath = (req.body && (typeof req.body.iconPath === 'string')) ? req.body.iconPath : null; |
| 4922 | const iconInfo = resolveCustomIconPath(iconPath, user); |
| 4923 | if ((iconInfo == null) || (iconInfo.isOwned !== true)) { res.status(400).json({ success: false, error: 'Invalid icon path.' }); return; } |
| 4924 | |
| 4925 | obj.fs.unlink(iconInfo.diskPath, function (err) { |
| 4926 | if (err && (err.code !== 'ENOENT')) { res.status(500).json({ success: false, error: 'Failed to delete icon.' }); return; } |
| 4927 | res.json({ success: true }); |
| 4928 | }); |
| 4929 | } |
| 4930 | |
| 4931 | function handleCustomIconDownload(req, res) { |
| 4932 | const domain = getDomain(req); |
| 4933 | if (domain == null) { res.sendStatus(404); return; } |
| 4934 | if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; } |
| 4935 | const user = obj.users[req.session.userid]; |
| 4936 | if (user == null) { res.sendStatus(401); return; } |
| 4937 | |
| 4938 | if ((req.params == null) || (typeof req.params[0] !== 'string')) { res.sendStatus(404); return; } |
| 4939 | const iconInfo = resolveCustomIconPath('/icons/custom/' + req.params[0], user); |
| 4940 | if (iconInfo == null) { res.sendStatus(404); return; } |
| 4941 | if ((iconInfo.isLegacy !== true) && (iconInfo.isOwned !== true)) { res.sendStatus(404); return; } |
| 4942 | const contentType = getCustomIconMimeType(iconInfo.iconName); |
| 4943 | if (contentType == null) { res.sendStatus(404); return; } |
| 4944 | |
| 4945 | obj.fs.readFile(iconInfo.diskPath, function (err, data) { |
| 4946 | if (err) { res.sendStatus(404); return; } |
| 4947 | const headers = { 'Content-Type': contentType, 'X-Content-Type-Options': 'nosniff' }; |
| 4948 | if (contentType === 'image/svg+xml') { headers['Content-Security-Policy'] = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'none'"; } |
| 4949 | res.set(headers); |
| 4950 | res.send(data); |
| 4951 | }); |
| 4952 | } |
| 4953 | |
| 4954 | function handleUploadFile(req, res) { |
| 4955 | const domain = checkUserIpAddress(req, res); |
| 4956 | if (domain == null) { return; } |
| 4957 | if (domain.userQuota == -1) { res.sendStatus(401); return; } |
| 4958 | var authUserid = null; |
| 4959 | if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } |
| 4960 | const multiparty = require('multiparty'); |
| 4961 | const form = new multiparty.Form(); |
| 4962 | form.parse(req, function (err, fields, files) { |
| 4963 | // If an authentication cookie is embedded in the form, use that. |
| 4964 | if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) { |
| 4965 | var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout |
| 4966 | if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding. |
| 4967 | if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication |
| 4968 | } |
| 4969 | if (authUserid == null) { res.sendStatus(401); return; } |
| 4970 | |
| 4971 | // Get the user |
| 4972 | const user = obj.users[authUserid]; |
| 4973 | if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights |
| 4974 | |
| 4975 | if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; } |
| 4976 | var xfile = null; |
| 4977 | try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { } |
| 4978 | if (xfile == null) { res.sendStatus(404); return; } |
| 4979 | // Get total bytes in the path |
| 4980 | var totalsize = readTotalFileSize(xfile.fullpath); |
| 4981 | if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken |
| 4982 | if (fields.name != null) { |
| 4983 | |
| 4984 | // See if we need to create the folder |
| 4985 | var domainx = 'domain'; |
| 4986 | if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; } |
| 4987 | try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { } |
| 4988 | try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { } |
| 4989 | try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { } |
| 4990 | |
| 4991 | // Upload method where all the file data is within the fields. |
| 4992 | var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*'); |
| 4993 | if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) { |
| 4994 | for (var i = 0; i < names.length; i++) { |
| 4995 | var originalName = names[i]; |
| 4996 | var safeName = obj.path.basename(originalName); |
| 4997 | if ((safeName !== originalName) || (obj.common.IsFilenameValid(safeName) == false)) { res.sendStatus(404); return; } |
| 4998 | var filedata = Buffer.from(datas[i].split(',')[1], 'base64'); |
| 4999 | if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file |
| 5000 | // Create the user folder if needed |
Showing first 5,000 of 10,886 lines.
View raw