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

This commit is contained in:
Ylian Saint-Hilaire 2022-07-03 00:44:58 -07:00
parent 4662a0a0b9
commit 5ba9d7e503
6 changed files with 123 additions and 29 deletions

View File

@ -220,7 +220,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
// Construct the HTTP request
var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
const blockedHeaders = ['origin', 'cookie']; // These are headers we do not forward
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
for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
var cookieStr = '';
for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }

View File

@ -86,12 +86,14 @@
"statsevents": { "type": "integer", "default": 2592000, "description": "Amount of time in seconds that server statistics are kept in the database." }
}
},
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 443 },
"portBind": { "type": "string", "description": "When set, bind the HTTPS main port to a specific network address." },
"aliasPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
"redirPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
"aliasPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": null },
"redirPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 80 },
"redirPortBind": { "type": "string", "description": "When set, bind the HTTP redirection port to a specific network address." },
"redirAliasPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
"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." },
"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." },
"agentPort": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "When set, enabled a new HTTPS server port that only accepts agent connections." },
"agentPortBind": { "type": "string", "description": "When set, binds the agent port to a specific network interface." },
"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." },

View File

@ -1647,7 +1647,7 @@ function CreateMeshCentralServer(config, args) {
if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
// Start the HTTP relay web server if needed
if ((obj.args.relayport != null) && (obj.args.relayport != 0)) {
if ((typeof obj.args.relaydns != 'string') && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
}

View File

@ -29,6 +29,8 @@
"_redirPort": 80,
"_redirPortBind": "127.0.0.1",
"_redirAliasPort": 80,
"_relayPort": 453,
"_relayDNS": "relay.myserver.mydomain.com",
"_agentPort": 1234,
"_agentPortBind": "127.0.0.1",
"_agentAliasPort": 1234,

View File

@ -1456,6 +1456,7 @@
var features2 = parseInt('{{{features2}}}');
var sessionTime = parseInt('{{{sessiontime}}}');
var webRelayPort = parseInt('{{{webRelayPort}}}');
var webRelayDns = '{{{webRelayDns}}}';
var sessionRefreshTimer = null;
var domain = '{{{domain}}}';
var domainUrl = '{{{domainurl}}}';
@ -8117,6 +8118,7 @@
}
var servername = serverinfo.name;
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.
if (webRelayDns != '') { servername = webRelayDns; }
var url = 'https://' + servername + ':' + webRelayPort + '/control-redirect.ashx?n=' + nodeid + '&p=' + port + '&appid=' + protocol; // Protocol: 1 = HTTP, 2 = HTTPS
if (addr != null) { url += '&addr=' + addr; }
if (relayid != null) { url += '&relayid=' + relayid; }

View File

@ -86,6 +86,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.renderLanguages = [];
obj.destroyedSessions = {};
// Web relay sessions
var webRelayNextSessionId = 1;
var webRelaySessions = {} // RelayID --> Web Mutli-Tunnel
var webRelayCleanupTimer = null;
// Mesh Rights
const MESHRIGHT_EDITMESH = 0x00000001;
const MESHRIGHT_MANAGEUSERS = 0x00000002;
@ -2859,7 +2864,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
amtscanoptions: amtscanoptions,
pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
webRelayPort: ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)
webRelayPort: ((typeof args.relaydns == 'string') ? args.port : ((parent.webrelayserver != null) ? parent.webrelayserver.port : 0)),
webRelayDns: ((typeof args.relaydns == 'string') ? args.relaydns : '')
}, dbGetFunc.req, domain), user);
}
xdbGetFunc.req = req;
@ -5725,7 +5731,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
}
}
}
obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
//obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
var sessionOptions = {
name: 'xid', // Recommended security practice to not use the default cookie name
httpOnly: true,
@ -5835,13 +5841,32 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
req.clientIp = ipex;
}
// If this is a web relay connection, handle it here.
if ((typeof obj.args.relaydns == 'string') && (req.headers.host == obj.args.relaydns) && (!req.url.startsWith('/control-redirect.ashx?n='))) {
// If this is a normal request (GET, POST, etc) handle it here
if ((req.session.userid != null) && (req.session.rid != null)) {
var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
if (relaySession != null) {
// The web relay session is valid, use it
relaySession.handleRequest(req, res);
} else {
// No web relay ession with this relay identifier, close the HTTP request.
res.sendStatus(404);
}
} else {
// The user is not logged in or does not have a relay identifier, close the HTTP request.
res.sendStatus(404);
}
return;
}
// Get the domain for this request
const domain = req.xdomain = getDomain(req);
parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
// Skip the rest is this is an agent connection
if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
// Setup security headers
const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
var selfurl = ' wss://' + req.headers.host;
@ -5951,27 +5976,27 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
} else {
// Present the login page as the root page
obj.app.get(url, handleRootRequest);
obj.app.post(url, handleRootPostRequest);
obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
}
obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', handleRestoreRequest); }
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
obj.app.get(url + 'terms', handleTermsRequest);
obj.app.get(url + 'xterm', handleXTermRequest);
obj.app.get(url + 'login', handleRootRequest);
obj.app.post(url + 'login', handleRootPostRequest);
obj.app.post(url + 'tokenlogin', handleLoginRequest);
obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
obj.app.get(url + 'logout', handleLogoutRequest);
obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
obj.app.post(url + 'changepassword', handlePasswordChangeRequest);
obj.app.post(url + 'deleteaccount', handleDeleteAccountRequest);
obj.app.post(url + 'createaccount', handleCreateAccountRequest);
obj.app.post(url + 'resetpassword', handleResetPasswordRequest);
obj.app.post(url + 'resetaccount', handleResetAccountRequest);
obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
obj.app.get(url + 'checkmail', handleCheckMailRequest);
obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest);
obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
obj.app.get(url + 'messenger', handleMessengerRequest);
obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
@ -5979,10 +6004,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
obj.app.post(url + 'uploadfile.ashx', handleUploadFile);
obj.app.post(url + 'uploadfilebatch.ashx', handleUploadFileBatch);
obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile);
obj.app.post(url + 'oneclickrecovery.ashx', handleOneClickRecoveryFile);
obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
@ -6013,7 +6038,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
obj.app.get(url + 'logo.png', handleLogoRequest);
obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
obj.app.post(url + 'translations', handleTranslationsRequest);
obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
obj.app.get(url + 'recordings.ashx', handleGetRecordings);
@ -6044,11 +6069,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
}
if (domain.agentinvitecodes == true) {
obj.app.get(url + 'invite', handleInviteRequest);
obj.app.post(url + 'invite', handleInviteRequest);
obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
}
if (parent.pluginHandler != null) {
obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
obj.app.post(url + 'pluginadmin.ashx', obj.handlePluginAdminPostReq);
obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
}
@ -6100,9 +6125,68 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
});
}
// Setup web relay on this web server if needed
// We set this up when a DNS name is used as a web relay instead of a port
if (typeof obj.args.relaydns == 'string') {
// This is the magic URL that will setup the relay session
obj.app.get('/control-redirect.ashx', function (req, res, next) {
if (req.headers.host != obj.args.relaydns) { res.sendStatus(404); return; }
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
if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
res.set({ 'Cache-Control': 'no-store' });
parent.debug('web', 'webRelaySetup');
// Check that all the required arguments are present
if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
// Get the user and domain information
const userid = req.session.userid;
const domainid = userid.split('/')[1];
const domain = parent.config.domains[domainid];
const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
const port = parseInt(req.query.p);
const appid = parseInt(req.query.appid);
// Check to see if we already have a multi-relay session that matches exactly this device and port for this user
var relaySession = null;
for (var i in webRelaySessions) {
const xrelaySession = webRelaySessions[i];
if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
relaySession = xrelaySession; // We found an exact match
}
}
if (relaySession != null) {
// Since we found a match, use it
req.session.rid = relaySession.sessionId;
} else {
// Create a web relay session
relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
relaySession.onclose = function (sessionId) {
// Remove the relay session
delete webRelaySessions[sessionId];
// If there are not more relay sessions, clear the cleanup timer
if ((Object.keys(webRelaySessions).length == 0) && (webRelayCleanupTimer != null)) { clearInterval(webRelayCleanupTimer); webRelayCleanupTimer = null; }
}
relaySession.sessionId = webRelayNextSessionId++;
// Set the multi-tunnel session
webRelaySessions[userid + '/' + relaySession.sessionId] = relaySession;
req.session.rid = relaySession.sessionId;
// Setup the cleanup timer if needed
if (webRelayCleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
}
// Redirect to root
res.redirect('/');
});
}
// Setup firebase push only server
if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', handleFirebasePushOnlyRelayRequest); }
if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
}
@ -6342,7 +6426,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('saml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-saml-callback', function (req, res, next) {
obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
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
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-intel-callback', function (req, res, next) {
obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
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
if (domain.passport == null) { next(); return; }
domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: '/', failureFlash: true })(req, res, next);
});
obj.app.post(url + 'auth-jumpcloud-callback', function (req, res, next) {
obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
var domain = getDomain(req);
if (domain.passport == null) { next(); return; }
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
return header + value + '\r\n';
}
// Check that everything is cleaned up
function checkWebRelaySessionsTimeout() {
for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
}
// Check that a cookie IP is within the correct range depending on the active policy
function checkCookieIp(cookieip, ip) {