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) {