From 9dac8b780793781c4d0d421a2c89fee6899e9fc1 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 8 Jul 2022 18:00:15 -0700 Subject: [PATCH] Web relay improvements, #4240 --- apprelays.js | 33 ++++++++++++++------ webrelayserver.js | 64 ++++++++++++++++++-------------------- webserver.js | 78 +++++++++++++++++++++++++---------------------- 3 files changed, 95 insertions(+), 80 deletions(-) diff --git a/apprelays.js b/apprelays.js index 2a33729a..3127eb58 100644 --- a/apprelays.js +++ b/apprelays.js @@ -84,6 +84,8 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, var tunnels = {}; var errorCount = 0; // If we keep closing tunnels without processing requests, fail the requests + parent.parent.debug('webrelay', 'CreateWebRelaySession, userid:' + userid + ', addr:' + addr + ', port:' + port); + // Any HTTP cookie set by the device is going to be shared between all tunnels to that device. obj.webCookies = {}; @@ -110,12 +112,14 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, // Handle new HTTP request obj.handleRequest = function (req, res) { + parent.parent.debug('webrelay', 'handleRequest, url:' + req.url); pendingRequests.push([req, res, false]); handleNextRequest(); } // Handle new websocket request obj.handleWebSocket = function (ws, req) { + parent.parent.debug('webrelay', 'handleWebSocket, url:' + req.url); pendingRequests.push([req, ws, true]); handleNextRequest(); } @@ -146,19 +150,26 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, function launchNewTunnel() { // Launch a new tunnel + parent.parent.debug('webrelay', 'launchNewTunnel'); const tunnel = module.exports.CreateWebRelay(obj, db, args, domain); tunnel.onclose = function (tunnelId, processedCount) { + if (tunnels == null) return; + parent.parent.debug('webrelay', 'tunnel-onclose'); if (processedCount == 0) { errorCount++; } // If this tunnel closed without processing any requests, mark this as an error delete tunnels[tunnelId]; handleNextRequest(); } tunnel.onconnect = function (tunnelId) { + if (tunnels == null) return; + parent.parent.debug('webrelay', 'tunnel-onconnect'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); } } } tunnel.oncompleted = function (tunnelId) { + if (tunnels == null) return; + parent.parent.debug('webrelay', 'tunnel-oncompleted'); errorCount = 0; // Something got completed, clear any error count if (pendingRequests.length > 0) { const x = pendingRequests.shift(); @@ -170,10 +181,14 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain, tunnels[tunnel.tunnelId] = tunnel; } + // Close all tunnels + obj.close = function () { close(); } + // Close all tunnels function close() { // Set the session as closed if (obj.closed == true) return; + parent.parent.debug('webrelay', 'tunnel-close'); obj.closed = true; // Close all tunnels @@ -291,7 +306,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { function sendWebSocketFrameToDevice(op, payload) { // Select a random mask - const mask = parent.parent.crypto.randomBytes(4) + const mask = parent.parent.parent.crypto.randomBytes(4) // Setup header and mask var header = null; @@ -376,7 +391,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { // Encode a cookie for the mesh relay const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port }; if (addr != null) { cookieContent.tcpaddr = addr; } - const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey); + const cookie = parent.parent.parent.encodeCookie(cookieContent, parent.parent.parent.loginCookieEncryptionKey); try { // Setup the correct URL with domain and use TLS only if needed. @@ -385,9 +400,9 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { var domainadd = ''; if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' } const url = protocol + '://localhost:' + args.port + '/' + domainadd + (((obj.mtype == 3) && (obj.relaynodeid == null)) ? 'local' : 'mesh') + 'relay.ashx?p=14&auth=' + cookie; // Protocol 14 is Web-TCP - parent.parent.debug('relay', 'TCP: Connection websocket to ' + url); + parent.parent.parent.debug('relay', 'TCP: Connection websocket to ' + url); obj.wsClient = new WebSocket(url, options); - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); }); + obj.wsClient.on('open', function () { parent.parent.parent.debug('relay', 'TCP: Relay websocket open'); }); obj.wsClient.on('message', function (data) { // Make sure to handle flow control. if (obj.tls) { // WS --> TLS @@ -402,13 +417,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel const tlsoptions = { socket: obj.ser, rejectUnauthorized: false }; obj.tls = require('tls').connect(tlsoptions, function () { - parent.parent.debug('relay', "Web Relay Secure TLS Connection"); + parent.parent.parent.debug('relay', "Web Relay Secure TLS Connection"); obj.relayActive = true; parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection }); obj.tls.setEncoding('binary'); - obj.tls.on('error', function (err) { parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); }); + obj.tls.on('error', function (err) { parent.parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); }); // Decrypted tunnel from TLS communcation to be forwarded to the browser obj.tls.on('data', function (data) { processHttpData(data); }); // TLS ---> Browser @@ -423,8 +438,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { processRawHttpData(data); } }); - obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); }); - obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); }); + obj.wsClient.on('close', function () { parent.parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); }); + obj.wsClient.on('error', function (err) { parent.parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); }); } catch (ex) { console.log(ex); } @@ -656,7 +671,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { // Send data thru the relay tunnel. Written to use TLS if needed. function send(data) { try { if (obj.tls) { obj.tls.write(data); } else { obj.wsClient.send(data); } } catch (ex) { } } - parent.parent.debug('relay', 'TCP: Request for web relay'); + parent.parent.parent.debug('relay', 'TCP: Request for web relay'); return obj; }; diff --git a/webrelayserver.js b/webrelayserver.js index 831cc6b8..f4b1d280 100644 --- a/webrelayserver.js +++ b/webrelayserver.js @@ -29,7 +29,6 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, obj.webRelayServer = null; obj.port = 0; obj.cleanupTimer = null; - var nextSessionId = 1; var relaySessions = {} // RelayID --> Web Mutli-Tunnel const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. var tlsSessionStore = {}; // Store TLS session information for quick resume. @@ -68,14 +67,14 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, httpOnly: true, keys: [args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances secure: (args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) - sameSite: args.sessionsamesite + sameSite: (args.sessionsamesite ? args.sessionsamesite : 'lax') } if (args.sessiontime != null) { sessionOptions.maxAge = (args.sessiontime * 60 * 1000); } obj.app.use(obj.session(sessionOptions)); // Add HTTP security headers to all responses obj.app.use(function (req, res, next) { - parent.debug('webrequest', req.url + ' (RelayServer)'); + parent.debug('webrelay', req.url); res.removeHeader('X-Powered-By'); res.set({ 'strict-transport-security': 'max-age=60000; includeSubDomains', @@ -83,7 +82,7 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, 'x-frame-options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'X-Content-Type-Options': 'nosniff', - 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';" + 'Content-Security-Policy': "default-src 'self'; style-src 'self' 'unsafe-inline';" }); // Set the real IP address of the request @@ -121,8 +120,8 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, return next(); } else { // If this is a normal request (GET, POST, etc) handle it here - if ((req.session.userid != null) && (req.session.rid != null)) { - var relaySession = relaySessions[req.session.userid + '/' + req.session.rid]; + if ((req.session.userid != null) && (req.session.x != null)) { + var relaySession = relaySessions[req.session.userid + '/' + req.session.x]; if (relaySession != null) { // The web relay session is valid, use it relaySession.handleRequest(req, res); @@ -154,8 +153,8 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, // Handle incoming web socket calls obj.app.ws('/*', function (ws, req) { - if ((req.session.userid != null) && (req.session.rid != null)) { - var relaySession = relaySessions[req.session.userid + '/' + req.session.rid]; + if ((req.session.userid != null) && (req.session.x != null)) { + var relaySession = relaySessions[req.session.userid + '/' + req.session.x]; if (relaySession != null) { // The multi-tunnel session is valid, use it relaySession.handleWebSocket(ws, req); @@ -173,10 +172,10 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, obj.app.get('/control-redirect.ashx', function (req, res) { if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; } res.set({ 'Cache-Control': 'no-store' }); - parent.debug('web', 'webRelaySetup'); + parent.debug('webrelay', '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; } + if ((req.session.userid == null) || (req.session.x == 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; @@ -188,36 +187,31 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, 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 relaySessions) { - const xrelaySession = relaySessions[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 - } + const xrelaySession = relaySessions[req.session.userid + '/' + req.session.x]; + if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) { + // We found an exact match, we are all setup already, redirect to root + res.redirect('/'); + return; } - 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 relaySessions[sessionId]; - // If there are not more relay sessions, clear the cleanup timer - if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; } - } - relaySession.sessionId = nextSessionId++; + // There is a relay session, but it's not correct, close it. + if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[req.session.userid + '/' + req.session.x]; } - // Set the multi-tunnel session - relaySessions[userid + '/' + relaySession.sessionId] = relaySession; - req.session.rid = relaySession.sessionId; - - // Setup the cleanup timer if needed - if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); } + // Create a web relay session + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid); + relaySession.onclose = function (sessionId) { + // Remove the relay session + delete relaySessions[sessionId]; + // If there are not more relay sessions, clear the cleanup timer + if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; } } + // Set the multi-tunnel session + relaySessions[userid + '/' + req.session.x] = relaySession; + + // Setup the cleanup timer if needed + if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); } + // Redirect to root res.redirect('/'); }); diff --git a/webserver.js b/webserver.js index 2e9aebe2..1de9992b 100644 --- a/webserver.js +++ b/webserver.js @@ -5758,11 +5758,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } } } - //obj.app.use(obj.bodyParser.urlencoded({ extended: false })); + + // Setup the cookie session var sessionOptions = { name: 'xid', // Recommended security practice to not use the default cookie name httpOnly: true, - domain: (certificates.CommonName != 'un-configured' ? "." + certificates.CommonName : null), keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html) sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax') @@ -5876,7 +5876,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } // If this is a web relay connection, handle it here. - if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) { return obj.webRelayRouter(req, res); } + if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) { + if (['GET', 'POST', 'PUT', 'HEAD'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; } + } // Get the domain for this request const domain = req.xdomain = getDomain(req); @@ -6590,7 +6592,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF 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; } + if ((req.session.userid == null) || (req.session.x == 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; @@ -6602,45 +6604,49 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF 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 - } + const xrelaySession = webRelaySessions[req.session.userid + '/' + req.session.x]; + if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) { + // We found an exact match, we are all setup already, redirect to root + res.redirect('/'); + return; } - 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); } + // There is a relay session, but it's not correct, close it. + if (xrelaySession != null) { + xrelaySession.close(); + delete webRelaySessions[req.session.userid + '/' + req.session.x]; } + // Create a web relay session + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, 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) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; } + } + + // Set the multi-tunnel session + webRelaySessions[userid + '/' + req.session.x] = relaySession; + + // Setup the cleanup timer if needed + if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); } + // Redirect to root res.redirect('/'); }); // Handle all incoming requests as web relays - obj.webRelayRouter.get('/*', function (req, res) { handleWebRelayRequest(req, res); }) + obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } }) // Handle all incoming requests as web relays - obj.webRelayRouter.post('/*', function (req, res) { handleWebRelayRequest(req, res); }) + obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } }) + + // Handle all incoming requests as web relays + obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } }) + + // Handle all incoming requests as web relays + obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } }) } // Indicates to ExpressJS that the override public folder should be used to serve static files. @@ -6685,8 +6691,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Handle an incoming request as a web relay function handleWebRelayRequest(req, res) { - if ((req.session.userid != null) && (req.session.rid != null)) { - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid]; + if ((req.session.userid != null) && (req.session.x != null)) { + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x]; if (relaySession != null) { // The web relay session is valid, use it relaySession.handleRequest(req, res); @@ -6702,8 +6708,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Handle an incoming websocket connection as a web relay function handleWebRelayWebSocket(ws, req) { - if ((req.session.userid != null) && (req.session.rid != null)) { - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid]; + if ((req.session.userid != null) && (req.session.x != null)) { + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x]; if (relaySession != null) { // The multi-tunnel session is valid, use it relaySession.handleWebSocket(ws, req);