| 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.2 |
| 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 | // ExpressJS login sample |
| 17 | // https://github.com/expressjs/express/blob/master/examples/auth/index.js |
| 18 | |
| 19 | // Construct a HTTP redirection web server object |
| 20 | module.exports.CreateRedirServer = function (parent, db, args, func) { |
| 21 | var obj = {}; |
| 22 | obj.parent = parent; |
| 23 | obj.db = db; |
| 24 | obj.args = args; |
| 25 | obj.certificates = null; |
| 26 | obj.express = require('express'); |
| 27 | obj.net = require('net'); |
| 28 | obj.app = obj.express(); |
| 29 | obj.tcpServer = null; |
| 30 | obj.port = null; |
| 31 | const leChallengePrefix = '/.well-known/acme-challenge/'; |
| 32 | |
| 33 | // Perform an HTTP to HTTPS redirection |
| 34 | function performRedirection(req, res) { |
| 35 | var host = req.headers.host; |
| 36 | if (typeof host == 'string') { host = host.split(':')[0]; } |
| 37 | if ((host == null) && (obj.certificates != null)) { host = obj.certificates.CommonName; if (obj.certificates.CommonName.indexOf('.') == -1) { host = req.headers.host; } } |
| 38 | var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified |
| 39 | res.redirect('https://' + host + ':' + httpsPort + req.url); |
| 40 | } |
| 41 | |
| 42 | // Setup CrowdSec bouncer middleware if needed |
| 43 | if (parent.crowdsecMiddleware != null) { obj.app.use(parent.crowdsecMiddleware); } |
| 44 | |
| 45 | /* |
| 46 | // Return the current domain of the request |
| 47 | function getDomain(req) { |
| 48 | var x = req.url.split("/"); |
| 49 | if (x.length < 2) { return parent.config.domains[""]; } |
| 50 | if (parent.config.domains[x[1].toLowerCase()]) { return parent.config.domains[x[1].toLowerCase()]; } |
| 51 | return parent.config.domains[""]; |
| 52 | } |
| 53 | */ |
| 54 | |
| 55 | // Renter the terms of service. |
| 56 | obj.app.get('/MeshServerRootCert.cer', function (req, res) { |
| 57 | // The redirection server starts before certificates are loaded, make sure to handle the case where no certificate is loaded now. |
| 58 | if (obj.certificates != null) { |
| 59 | res.set({ 'Cache-Control': 'no-store', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename*="' + encodeURIComponent(obj.certificates.RootName) + '.cer"' }); |
| 60 | var rootcert = obj.certificates.root.cert; |
| 61 | var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n'); |
| 62 | if (i >= 0) { rootcert = rootcert.substring(i + 29); } |
| 63 | i = rootcert.indexOf('-----END CERTIFICATE-----'); |
| 64 | if (i >= 0) { rootcert = rootcert.substring(i, 0); } |
| 65 | res.send(Buffer.from(rootcert, 'base64')); |
| 66 | } else { |
| 67 | res.sendStatus(404); |
| 68 | } |
| 69 | }); |
| 70 | |
| 71 | // Add HTTP security headers to all responses |
| 72 | obj.app.use(function (req, res, next) { |
| 73 | parent.debug('webrequest', req.url + ' (RedirServer)'); |
| 74 | res.removeHeader('X-Powered-By'); |
| 75 | |
| 76 | if ((parent.letsencrypt != null) && (req.url.startsWith(leChallengePrefix))) { |
| 77 | // Let's Encrypt Support |
| 78 | parent.letsencrypt.challenge(req.url.slice(leChallengePrefix.length), getCleanHostname(req), function (response) { if (response == null) { res.sendStatus(404); } else { res.send(response); } }); |
| 79 | } else { |
| 80 | // Everything else |
| 81 | var selfurl = (' wss://' + req.headers.host); |
| 82 | res.set({ |
| 83 | 'strict-transport-security': 'max-age=60000; includeSubDomains', |
| 84 | 'Referrer-Policy': 'no-referrer', |
| 85 | 'x-frame-options': 'SAMEORIGIN', |
| 86 | 'X-XSS-Protection': '1; mode=block', |
| 87 | 'X-Content-Type-Options': 'nosniff', |
| 88 | 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';" |
| 89 | }); |
| 90 | return next(); |
| 91 | } |
| 92 | }); |
| 93 | |
| 94 | // Once the main web server is started, call this to hookup additional handlers |
| 95 | obj.hookMainWebServer = function (certs) { |
| 96 | obj.certificates = certs; |
| 97 | for (var i in parent.config.domains) { |
| 98 | if (parent.config.domains[i].dns != null) { continue; } |
| 99 | var url = parent.config.domains[i].url; |
| 100 | obj.app.post(url + 'amtevents.ashx', obj.parent.webserver.handleAmtEventRequest); |
| 101 | obj.app.get(url + 'meshsettings', obj.parent.webserver.handleMeshSettingsRequest); |
| 102 | obj.app.get(url + 'meshagents', obj.parent.webserver.handleMeshAgentRequest); |
| 103 | |
| 104 | // Server redirects |
| 105 | if (parent.config.domains[i].redirects) { |
| 106 | for (var j in parent.config.domains[i].redirects) { |
| 107 | if (j[0] != '_') { obj.app.get(url + j, obj.parent.webserver.handleDomainRedirect); } |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // Setup all HTTP redirection handlers |
| 114 | //obj.app.set("etag", false); |
| 115 | for (var i in parent.config.domains) { |
| 116 | if (parent.config.domains[i].dns != null) { continue; } |
| 117 | var url = parent.config.domains[i].url; |
| 118 | obj.app.get(url, performRedirection); // Root redirection |
| 119 | |
| 120 | // Setup any .well-known folders |
| 121 | var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id))); |
| 122 | if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); } |
| 123 | |
| 124 | // Setup all of the redirections to HTTPS |
| 125 | const redirections = ['player.htm', 'terms', 'logout', 'MeshServerRootCert.cer', 'mescript.ashx', 'checkmail', 'agentinvite', 'messenger', 'meshosxagent', 'devicepowerevents.ashx', 'downloadfile.ashx', 'userfiles/*', 'webrelay.ashx', 'health.ashx', 'logo.png', 'welcome.jpg', 'invite']; |
| 126 | for (i in redirections) { obj.app.get(url + redirections[i], performRedirection); } |
| 127 | } |
| 128 | |
| 129 | // Find a free port starting with the specified one and going up. |
| 130 | function CheckListenPort(port, addr, func) { |
| 131 | var s = obj.net.createServer(function (socket) { }); |
| 132 | obj.tcpServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on("error", function (err) { |
| 133 | if (args.exactports) { console.error("ERROR: MeshCentral HTTP server port " + port + " not available."); process.exit(); } |
| 134 | else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } } |
| 135 | }); |
| 136 | } |
| 137 | |
| 138 | // Start the ExpressJS web server, if the port is busy try the next one. |
| 139 | function StartRedirServer(port, addr) { |
| 140 | if (port == 0 || port == 65535) { return; } |
| 141 | obj.tcpServer = obj.app.listen(port, addr, function () { |
| 142 | obj.port = port; |
| 143 | console.log("MeshCentral HTTP redirection server running on port " + port + "."); |
| 144 | obj.parent.authLog('http', 'Server listening on ' + ((addr != null)?addr:'0.0.0.0') + ' port ' + port + '.'); |
| 145 | obj.parent.updateServerState('redirect-port', port); |
| 146 | func(obj.port); |
| 147 | }).on('error', function (err) { |
| 148 | if ((err.code == 'EACCES') && (port < 65535)) { StartRedirServer(port + 1, addr); } else { console.log(err); func(obj.port); } |
| 149 | }); |
| 150 | } |
| 151 | |
| 152 | // Get the remote hostname correctly |
| 153 | const servernameRe = /^[a-z0-9\.\-]+$/i; |
| 154 | function getHostname(req) { return req.hostname || req.headers['x-forwarded-host'] || (req.headers.host || ''); }; |
| 155 | function getCleanHostname(req) { |
| 156 | var servername = getHostname(req).toLowerCase().replace(/:.*/, ''); |
| 157 | try { req.hostname = servername; } catch (e) { } // read-only express property |
| 158 | if (req.headers['x-forwarded-host']) { req.headers['x-forwarded-host'] = servername; } |
| 159 | try { req.headers.host = servername; } catch (e) { } |
| 160 | return (servernameRe.test(servername) && -1 === servername.indexOf('..') && servername) || ''; |
| 161 | }; |
| 162 | |
| 163 | CheckListenPort(args.redirport, args.redirportbind, StartRedirServer); |
| 164 | |
| 165 | return obj; |
| 166 | }; |