Improved DNS based web relay, #4210

Ylian Saint-Hilaire committed Jul 3, 2022 at 18:17 UTC b33900dfbf79731b282df62c322bade86f76383c
2 files changed +115 -79
apprelays.js
+5 -1
@@ -218,9 +218,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
218 if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
219 parent.lastOperation = obj.lastOperation = Date.now();
220
221 + // Check if this is a websocket
222 + if (req.headers['upgrade'] == 'websocket') { console.log('Attempt to process a websocket in HTTP tunnel method.'); res.end(); return false; }
223 +
224 // Construct the HTTP request
225 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
223 - const blockedHeaders = ['origin', 'cookie', 'upgrade-insecure-requests', 'sec-ch-ua', 'sec-ch-ua-mobile', 'dnt', 'sec-fetch-user', 'sec-ch-ua-platform', 'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest']; // These are headers we do not forward
226 + const blockedHeaders = ['host', 'origin', 'cookie', 'upgrade-insecure-requests', 'sec-ch-ua', 'sec-ch-ua-mobile', 'dnt', 'sec-fetch-user', 'sec-ch-ua-platform', 'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest']; // These are headers we do not forward
227 + request += 'host: central.mesh.meshcentral.com\r\n';
228 for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
229 var cookieStr = '';
230 for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
webserver.js
+110 -78
@@ -13,7 +13,7 @@
13 /*jshint esversion: 6 */
14 'use strict';
15
16 -// SerialTunnel object is used to embed TLS within another connection.
16 +// SerialTunnel object is used to embed TLS within another connection.e
17 function SerialTunnel(options) {
18 var obj = new require('stream').Duplex(options);
19 obj.forwardwrite = null;
@@ -5742,6 +5742,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5742 if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); }
5743 obj.app.use(obj.session(sessionOptions));
5744
5745 + // Handle all incoming web sockets, see if some need to be handled as web relays
5746 + obj.app.ws('/*', function (ws, req, next) {
5747 + if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) { handleWebRelayWebSocket(ws, req); return; }
5748 + return next();
5749 + });
5750 +
5751 // Add HTTP security headers to all responses
5752 obj.app.use(function (req, res, next) {
5753 // Check if a session is destroyed
@@ -5842,23 +5848,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5848 }
5849
5850 // If this is a web relay connection, handle it here.
5845 - if ((typeof obj.args.relaydns == 'string') && (req.headers.host == obj.args.relaydns) && (!req.url.startsWith('/control-redirect.ashx?n='))) {
5846 - // If this is a normal request (GET, POST, etc) handle it here
5847 - if ((req.session.userid != null) && (req.session.rid != null)) {
5848 - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
5849 - if (relaySession != null) {
5850 - // The web relay session is valid, use it
5851 - relaySession.handleRequest(req, res);
5852 - } else {
5853 - // No web relay ession with this relay identifier, close the HTTP request.
5854 - res.sendStatus(404);
5855 - }
5856 - } else {
5857 - // The user is not logged in or does not have a relay identifier, close the HTTP request.
5858 - res.sendStatus(404);
5859 - }
5860 - return;
5861 - }
5851 + if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) { return obj.webRelayRouter(req, res); }
5852
5853 // Get the domain for this request
5854 const domain = req.xdomain = getDomain(req);
@@ -6125,65 +6115,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6115 });
6116 }
6117
6128 - // Setup web relay on this web server if needed
6129 - // We set this up when a DNS name is used as a web relay instead of a port
6130 - if (typeof obj.args.relaydns == 'string') {
6131 - // This is the magic URL that will setup the relay session
6132 - obj.app.get('/control-redirect.ashx', function (req, res, next) {
6133 - if (req.headers.host != obj.args.relaydns) { res.sendStatus(404); return; }
6134 - if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
6135 - if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
6136 - res.set({ 'Cache-Control': 'no-store' });
6137 - parent.debug('web', 'webRelaySetup');
6138 -
6139 - // Check that all the required arguments are present
6140 - if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
6141 -
6142 - // Get the user and domain information
6143 - const userid = req.session.userid;
6144 - const domainid = userid.split('/')[1];
6145 - const domain = parent.config.domains[domainid];
6146 - const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6147 - const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6148 - const port = parseInt(req.query.p);
6149 - const appid = parseInt(req.query.appid);
6150 -
6151 - // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
6152 - var relaySession = null;
6153 - for (var i in webRelaySessions) {
6154 - const xrelaySession = webRelaySessions[i];
6155 - if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6156 - relaySession = xrelaySession; // We found an exact match
6157 - }
6158 - }
6159 -
6160 - if (relaySession != null) {
6161 - // Since we found a match, use it
6162 - req.session.rid = relaySession.sessionId;
6163 - } else {
6164 - // Create a web relay session
6165 - relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
6166 - relaySession.onclose = function (sessionId) {
6167 - // Remove the relay session
6168 - delete webRelaySessions[sessionId];
6169 - // If there are not more relay sessions, clear the cleanup timer
6170 - if ((Object.keys(webRelaySessions).length == 0) && (webRelayCleanupTimer != null)) { clearInterval(webRelayCleanupTimer); webRelayCleanupTimer = null; }
6171 - }
6172 - relaySession.sessionId = webRelayNextSessionId++;
6173 -
6174 - // Set the multi-tunnel session
6175 - webRelaySessions[userid + '/' + relaySession.sessionId] = relaySession;
6176 - req.session.rid = relaySession.sessionId;
6177 -
6178 - // Setup the cleanup timer if needed
6179 - if (webRelayCleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6180 - }
6181 -
6182 - // Redirect to root
6183 - res.redirect('/');
6184 - });
6185 - }
6186 -
6118 // Setup firebase push only server
6119 if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
6120 if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
@@ -6515,7 +6446,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6446 }
6447 }
6448 }
6518 -
6449 }
6450
6451 // Server redirects
@@ -6609,6 +6539,73 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6539 obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
6540 }
6541
6542 + // Setup web relay on this web server if needed
6543 + // We set this up when a DNS name is used as a web relay instead of a port
6544 + if (typeof obj.args.relaydns == 'string') {
6545 + obj.webRelayRouter = require('express').Router();
6546 +
6547 + // This is the magic URL that will setup the relay session
6548 + obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
6549 + if (req.headers.host != obj.args.relaydns) { res.sendStatus(404); return; }
6550 + if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
6551 + if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
6552 + res.set({ 'Cache-Control': 'no-store' });
6553 + parent.debug('web', 'webRelaySetup');
6554 +
6555 + // Check that all the required arguments are present
6556 + if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
6557 +
6558 + // Get the user and domain information
6559 + const userid = req.session.userid;
6560 + const domainid = userid.split('/')[1];
6561 + const domain = parent.config.domains[domainid];
6562 + const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
6563 + const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
6564 + const port = parseInt(req.query.p);
6565 + const appid = parseInt(req.query.appid);
6566 +
6567 + // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
6568 + var relaySession = null;
6569 + for (var i in webRelaySessions) {
6570 + const xrelaySession = webRelaySessions[i];
6571 + if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6572 + relaySession = xrelaySession; // We found an exact match
6573 + }
6574 + }
6575 +
6576 + if (relaySession != null) {
6577 + // Since we found a match, use it
6578 + req.session.rid = relaySession.sessionId;
6579 + } else {
6580 + // Create a web relay session
6581 + relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
6582 + relaySession.onclose = function (sessionId) {
6583 + // Remove the relay session
6584 + delete webRelaySessions[sessionId];
6585 + // If there are not more relay sessions, clear the cleanup timer
6586 + if ((Object.keys(webRelaySessions).length == 0) && (webRelayCleanupTimer != null)) { clearInterval(webRelayCleanupTimer); webRelayCleanupTimer = null; }
6587 + }
6588 + relaySession.sessionId = webRelayNextSessionId++;
6589 +
6590 + // Set the multi-tunnel session
6591 + webRelaySessions[userid + '/' + relaySession.sessionId] = relaySession;
6592 + req.session.rid = relaySession.sessionId;
6593 +
6594 + // Setup the cleanup timer if needed
6595 + if (webRelayCleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6596 + }
6597 +
6598 + // Redirect to root
6599 + res.redirect('/');
6600 + });
6601 +
6602 + // Handle all incoming requests as web relays
6603 + obj.webRelayRouter.get('/*', function (req, res) { handleWebRelayRequest(req, res); })
6604 +
6605 + // Handle all incoming requests as web relays
6606 + obj.webRelayRouter.post('/*', function (req, res) { handleWebRelayRequest(req, res); })
6607 + }
6608 +
6609 // Indicates to ExpressJS that the override public folder should be used to serve static files.
6610 if (parent.config.domains[i].webpublicpath != null) {
6611 // Use domain public path
@@ -6648,6 +6645,41 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6645 if (doneFunc) doneFunc();
6646 }
6647
6648 +
6649 + // Handle an incoming request as a web relay
6650 + function handleWebRelayRequest(req, res) {
6651 + if ((req.session.userid != null) && (req.session.rid != null)) {
6652 + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
6653 + if (relaySession != null) {
6654 + // The web relay session is valid, use it
6655 + relaySession.handleRequest(req, res);
6656 + } else {
6657 + // No web relay ession with this relay identifier, close the HTTP request.
6658 + res.sendStatus(404);
6659 + }
6660 + } else {
6661 + // The user is not logged in or does not have a relay identifier, close the HTTP request.
6662 + res.sendStatus(404);
6663 + }
6664 + }
6665 +
6666 + // Handle an incoming websocket connection as a web relay
6667 + function handleWebRelayWebSocket(ws, req) {
6668 + if ((req.session.userid != null) && (req.session.rid != null)) {
6669 + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
6670 + if (relaySession != null) {
6671 + // The multi-tunnel session is valid, use it
6672 + relaySession.handleWebSocket(ws, req);
6673 + } else {
6674 + // No multi-tunnel session with this relay identifier, close the websocket.
6675 + ws.close();
6676 + }
6677 + } else {
6678 + // The user is not logged in or does not have a relay identifier, close the websocket.
6679 + ws.close();
6680 + }
6681 + }
6682 +
6683 // Perform server inner authentication
6684 // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
6685 // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username