Added support to HTTP web relay on the main web server port with used with a specified DNS name, #4210

Ylian Saint-Hilaire committed Jul 3, 2022 at 00:44 UTC 5ba9d7e5030e950691adb0578772e06e7780c9c6
6 files changed +123 -29
apprelays.js
+1 -1
@@ -220,7 +220,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
220
221 // Construct the HTTP request
222 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
223 - const blockedHeaders = ['origin', 'cookie']; // These are headers we do not forward
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
224 for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
225 var cookieStr = '';
226 for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
meshcentral-config-schema.json
+5 -3
@@ -86,12 +86,14 @@
86 "statsevents": { "type": "integer", "default": 2592000, "description": "Amount of time in seconds that server statistics are kept in the database." }
87 }
88 },
89 - "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
89 + "port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 443 },
90 "portBind": { "type": "string", "description": "When set, bind the HTTPS main port to a specific network address." },
91 - "aliasPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
92 - "redirPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
91 + "aliasPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": null },
92 + "redirPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 80 },
93 "redirPortBind": { "type": "string", "description": "When set, bind the HTTP redirection port to a specific network address." },
94 "redirAliasPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
95 + "relayPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": null, "description": "When set, a web relay web server is bound to this port and will allow user access to remote web sites." },
96 + "relayDNS": { "type": "string", "default": null, "description": "When set, relayPort valie is ignored. Set this to a DNS name the points to this server. When the server is accessed using the DNS name, the main web server port is used as a web relay port." },
97 "agentPort": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "When set, enabled a new HTTPS server port that only accepts agent connections." },
98 "agentPortBind": { "type": "string", "description": "When set, binds the agent port to a specific network interface." },
99 "agentAliasPort": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "When set, indicates the actual publically visible agent-only port. If not set, the AgentPort value is used." },
meshcentral.js
+1 -1
@@ -1647,7 +1647,7 @@ function CreateMeshCentralServer(config, args) {
1647 if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
1648
1649 // Start the HTTP relay web server if needed
1650 - if ((obj.args.relayport != null) && (obj.args.relayport != 0)) {
1650 + if ((typeof obj.args.relaydns != 'string') && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
1651 obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
1652 }
1653
sample-config-advanced.json
+2
@@ -29,6 +29,8 @@
29 "_redirPort": 80,
30 "_redirPortBind": "127.0.0.1",
31 "_redirAliasPort": 80,
32 + "_relayPort": 453,
33 + "_relayDNS": "relay.myserver.mydomain.com",
34 "_agentPort": 1234,
35 "_agentPortBind": "127.0.0.1",
36 "_agentAliasPort": 1234,
views/default.handlebars
+2
@@ -1456,6 +1456,7 @@
1456 var features2 = parseInt('{{{features2}}}');
1457 var sessionTime = parseInt('{{{sessiontime}}}');
1458 var webRelayPort = parseInt('{{{webRelayPort}}}');
1459 + var webRelayDns = '{{{webRelayDns}}}';
1460 var sessionRefreshTimer = null;
1461 var domain = '{{{domain}}}';
1462 var domainUrl = '{{{domainurl}}}';
@@ -8117,6 +8118,7 @@
8118 }
8119 var servername = serverinfo.name;
8120 if ((servername.indexOf('.') == -1) || ((features & 2) != 0)) { servername = window.location.hostname; } // If the server name is not set or it's in LAN-only mode, use the URL hostname as server name.
8121 + if (webRelayDns != '') { servername = webRelayDns; }
8122 var url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol; // Protocol: 1 = HTTP, 2 = HTTPS
8123 if (addr != null) { url += '&addr=' + addr; }
8124 if (relayid != null) { url += '&relayid=' + relayid; }
webserver.js
+112 -24
@@ -86,6 +86,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
86 obj.renderLanguages = [];
87 obj.destroyedSessions = {};
88
89 + // Web relay sessions
90 + var webRelayNextSessionId = 1;
91 + var webRelaySessions = {} // RelayID --> Web Mutli-Tunnel
92 + var webRelayCleanupTimer = null;
93 +
94 // Mesh Rights
95 const MESHRIGHT_EDITMESH = 0x00000001;
96 const MESHRIGHT_MANAGEUSERS = 0x00000002;
@@ -2859,7 +2864,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2864 webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
2865 amtscanoptions: amtscanoptions,
2866 pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
2862 - webRelayPort: ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)
2867 + webRelayPort: ((typeof args.relaydns == 'string') ? args.port : ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)),
2868 + webRelayDns: ((typeof args.relaydns == 'string') ? args.relaydns : '')
2869 }, dbGetFunc.req, domain), user);
2870 }
2871 xdbGetFunc.req = req;
@@ -5725,7 +5731,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5731 }
5732 }
5733 }
5728 - obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
5734 + //obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
5735 var sessionOptions = {
5736 name: 'xid', // Recommended security practice to not use the default cookie name
5737 httpOnly: true,
@@ -5835,13 +5841,32 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5841 req.clientIp = ipex;
5842 }
5843
5844 + // 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 + }
5862 +
5863 // Get the domain for this request
5864 const domain = req.xdomain = getDomain(req);
5865 parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
5866
5867 // Skip the rest is this is an agent connection
5868 if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
5844 -
5869 +
5870 // Setup security headers
5871 const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
5872 var selfurl = ' wss://' + req.headers.host;
@@ -5951,27 +5976,27 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5976 } else {
5977 // Present the login page as the root page
5978 obj.app.get(url, handleRootRequest);
5954 - obj.app.post(url, handleRootPostRequest);
5979 + obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
5980 }
5981 obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
5982 if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
5958 - if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', handleRestoreRequest); }
5983 + if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
5984 obj.app.get(url + 'terms', handleTermsRequest);
5985 obj.app.get(url + 'xterm', handleXTermRequest);
5986 obj.app.get(url + 'login', handleRootRequest);
5962 - obj.app.post(url + 'login', handleRootPostRequest);
5963 - obj.app.post(url + 'tokenlogin', handleLoginRequest);
5987 + obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
5988 + obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
5989 obj.app.get(url + 'logout', handleLogoutRequest);
5990 obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
5966 - obj.app.post(url + 'changepassword', handlePasswordChangeRequest);
5967 - obj.app.post(url + 'deleteaccount', handleDeleteAccountRequest);
5968 - obj.app.post(url + 'createaccount', handleCreateAccountRequest);
5969 - obj.app.post(url + 'resetpassword', handleResetPasswordRequest);
5970 - obj.app.post(url + 'resetaccount', handleResetAccountRequest);
5991 + obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
5992 + obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
5993 + obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
5994 + obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
5995 + obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
5996 obj.app.get(url + 'checkmail', handleCheckMailRequest);
5997 obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
5998 obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
5974 - obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest);
5999 + obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
6000 obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
6001 obj.app.get(url + 'messenger', handleMessengerRequest);
6002 obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
@@ -5979,10 +6004,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6004 obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
6005 obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
6006 obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
5982 - obj.app.post(url + 'uploadfile.ashx', handleUploadFile);
5983 - obj.app.post(url + 'uploadfilebatch.ashx', handleUploadFileBatch);
5984 - obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile);
5985 - obj.app.post(url + 'oneclickrecovery.ashx', handleOneClickRecoveryFile);
6007 + obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
6008 + obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
6009 + obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
6010 + obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
6011 obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
6012 obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
6013 obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
@@ -6013,7 +6038,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6038 obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
6039 obj.app.get(url + 'logo.png', handleLogoRequest);
6040 obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
6016 - obj.app.post(url + 'translations', handleTranslationsRequest);
6041 + obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
6042 obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
6043 obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
6044 obj.app.get(url + 'recordings.ashx', handleGetRecordings);
@@ -6044,11 +6069,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6069 }
6070 if (domain.agentinvitecodes == true) {
6071 obj.app.get(url + 'invite', handleInviteRequest);
6047 - obj.app.post(url + 'invite', handleInviteRequest);
6072 + obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
6073 }
6074 if (parent.pluginHandler != null) {
6075 obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
6051 - obj.app.post(url + 'pluginadmin.ashx', obj.handlePluginAdminPostReq);
6076 + obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
6077 obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
6078 }
6079
@@ -6100,9 +6125,68 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6125 });
6126 }
6127
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 +
6187 // Setup firebase push only server
6188 if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
6105 - if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', handleFirebasePushOnlyRelayRequest); }
6189 + if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
6190 if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
6191 }
6192
@@ -6342,7 +6426,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6426 if (domain.passport == null) { next(); return; }
6427 domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6428 });
6345 - obj.app.post(url + 'auth-saml-callback', function (req, res, next) {
6429 + obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6430 var domain = getDomain(req);
6431 if (domain.passport == null) { next(); return; }
6432 domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
@@ -6384,7 +6468,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6468 if (domain.passport == null) { next(); return; }
6469 domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6470 });
6387 - obj.app.post(url + 'auth-intel-callback', function (req, res, next) {
6471 + obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6472 var domain = getDomain(req);
6473 if (domain.passport == null) { next(); return; }
6474 domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
@@ -6423,7 +6507,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6507 if (domain.passport == null) { next(); return; }
6508 domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
6509 });
6426 - obj.app.post(url + 'auth-jumpcloud-callback', function (req, res, next) {
6510 + obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
6511 var domain = getDomain(req);
6512 if (domain.passport == null) { next(); return; }
6513 domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
@@ -8263,6 +8347,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8347 return header + value + '\r\n';
8348 }
8349
8350 + // Check that everything is cleaned up
8351 + function checkWebRelaySessionsTimeout() {
8352 + for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
8353 + }
8354
8355 // Check that a cookie IP is within the correct range depending on the active policy
8356 function checkCookieIp(cookieip, ip) {