From 3b93d1adf7a591f9b8352e800c7ee6a3d8b0cca9 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 25 Jun 2022 16:22:51 -0700 Subject: [PATCH] Web relay improvements, #4172 --- apprelays.js | 61 ++++++++++++++++++++++++++++------------------- webrelayserver.js | 40 ++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/apprelays.js b/apprelays.js index 8d9b857e..3046db38 100644 --- a/apprelays.js +++ b/apprelays.js @@ -63,20 +63,27 @@ const MESHRIGHT_ADMIN = 0xFFFFFFFF; // Construct a Web relay object module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) { const obj = {}; + obj.parent = parent; obj.lastOperation = Date.now(); + obj.domain = domain; obj.userid = userid; + obj.nodeid = nodeid; + obj.addr = addr; + obj.port = port; var pendingRequests = []; - var activeRequests = 0; var nextTunnelId = 1; var tunnels = {}; + // Any HTTP cookie set by the device is going to be shared between all tunnels to that device. + obj.webCookie = null; + // Events obj.closed = false; obj.onclose = null; // Handle new HTTP request obj.handleRequest = function (req, res) { - console.log('handleRequest', req.url); + //console.log('handleRequest', req.url); pendingRequests.push([req, res]); handleNextRequest(); } @@ -89,7 +96,7 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us count += (tunnels[i].isWebSocket ? 0 : 1); if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) { // Found a free tunnel, use it - console.log('handleNextRequest-found empty tunnel'); + //console.log('handleNextRequest-found empty tunnel'); const x = pendingRequests.shift(); tunnels[i].processRequest(x[0], x[1]); return; @@ -99,11 +106,11 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us if (count > 0) return; // Launch a new tunnel - console.log('handleNextRequest-starting new tunnel'); - const tunnel = module.exports.CreateWebRelay(parent, db, args, domain); - tunnel.onclose = function (tunnelId) { console.log('tclose'); delete tunnels[tunnelId]; } - tunnel.onconnect = function (tunnelId) { console.log('tconnect'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } - tunnel.oncompleted = function (tunnelId) { console.log('tcompleted'); if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } + //console.log('handleNextRequest-starting new tunnel'); + const tunnel = module.exports.CreateWebRelay(obj, db, args, domain); + tunnel.onclose = function (tunnelId) { delete tunnels[tunnelId]; } + tunnel.onconnect = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } + tunnel.oncompleted = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } } tunnel.connect(userid, nodeid, addr, port); tunnel.tunnelId = nextTunnelId++; tunnels[tunnel.tunnelId] = tunnel; @@ -144,26 +151,27 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { obj.processRequest = function (req, res) { if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; } - console.log('processRequest-start', req.method); + //console.log('processRequest-start', req.method); // Construct the HTTP request var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n'; request += 'host: ' + obj.addr + ':' + obj.port + '\r\n'; - for (var i in req.headers) { - const li = i.toLowerCase(); - if ((li != 'origin') && (li != 'host')) { request += i + ': ' + req.headers[i] + '\r\n'; } - } + const blockedHeaders = ['origin', 'host', 'cookie']; // 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'; } } + if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it. request += '\r\n'; + //console.log('request', request); + if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) { // Read the HTTP body and send the request to the device obj.requestBinary = [Buffer.from(request)]; req.on('data', function (data) { obj.requestBinary.push(data); }); - req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; console.log('processRequest-sent-withbody'); }); + req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; }); } else { // Request has no body, send it now obj.wsClient.send(Buffer.from(request)); - console.log('processRequest-sent-nobody'); + //console.log('processRequest-sent-nobody'); } obj.res = res; } @@ -173,6 +181,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { if (obj.closed == true) return; obj.closed = true; + /* // Event the session ending if ((obj.startTime) && (obj.meshid != null)) { // Collect how many raw bytes where received and sent. @@ -187,6 +196,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { delete obj.startTime; delete obj.sessionid; } + */ if (obj.wsClient) { obj.wsClient.removeAllListeners('open'); obj.wsClient.removeAllListeners('message'); @@ -213,7 +223,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.encodeCookie(cookieContent, parent.loginCookieEncryptionKey); + const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey); try { // Setup the correct URL with domain and use TLS only if needed. @@ -222,9 +232,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.debug('relay', 'TCP: Connection websocket to ' + url); + parent.parent.debug('relay', 'TCP: Connection websocket to ' + url); obj.wsClient = new WebSocket(url, options); - obj.wsClient.on('open', function () { parent.debug('relay', 'TCP: Relay websocket open'); }); + obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); }); obj.wsClient.on('message', function (data) { // Make sure to handle flow control. if (obj.relayActive == false) { if ((data == 'c') || (data == 'cr')) { @@ -243,8 +253,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { processHttpData(data.toString('binary')); } }); - obj.wsClient.on('close', function () { parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); }); - obj.wsClient.on('error', function (err) { parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); }); + 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(); }); } catch (ex) { console.log(ex); } @@ -310,10 +320,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { // This is a fully parsed HTTP response from the remote device function processHttpResponse(header, data) { - console.log('processHttpResponse'); - + //console.log('processHttpResponse', header); obj.res.status(parseInt(header.Directive[1])); // Set the status - for (var i in header) { if (i != 'Directive') { obj.res.set(i, header[i]); } } // Set the headers + const blockHeaders = ['Directive' ]; // These are headers we do not forward + for (var i in header) { + if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it + else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked + } obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future obj.res.end(data, 'binary'); // Write the data delete obj.res; @@ -329,7 +342,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { return true; } - parent.debug('relay', 'TCP: Request for web relay'); + parent.parent.debug('relay', 'TCP: Request for web relay'); return obj; }; diff --git a/webrelayserver.js b/webrelayserver.js index 274f8829..7287be64 100644 --- a/webrelayserver.js +++ b/webrelayserver.js @@ -116,8 +116,15 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, } else { if ((req.session.userid != null) && (req.session.rid != null)) { var relayMultiTunnel = relayMultiTunnels[req.session.userid + '/' + req.session.rid]; - if (relayMultiTunnel != null) { relayMultiTunnel.handleRequest(req, res); return; } + if (relayMultiTunnel != null) { + // The multi-tunnel session is valid, use it + relayMultiTunnel.handleRequest(req, res); + } else { + // No multi-tunnel session with this relay identifier, close the HTTP request. + res.end(); + } } else { + // The user is not logged in or does not have a relay identifier, close the HTTP request. res.end(); } } @@ -136,15 +143,32 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, 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); - // Create the multi-tunnel - const relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, ((req.query.relayid != null) ? req.query.relayid : req.query.n), (req.query.addr != null) ? req.query.addr : '127.0.0.1', parseInt(req.query.p)); - relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; } - relayMultiTunnel.multiTunnelId = nextMultiTunnelId++; + // Check to see if we already have a multi-relay session that matches exactly this device and port for this user + var relayMultiTunnel = null; + for (var i in relayMultiTunnels) { + const xrelayMultiTunnel = relayMultiTunnels[i]; + if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port)) { + relayMultiTunnel = xrelayMultiTunnel; // We found an exact match + } + } - // Set the tunnel - relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel; - req.session.rid = relayMultiTunnel.multiTunnelId; + if (relayMultiTunnel != null) { + // Since we found a match, use it + req.session.rid = relayMultiTunnel.multiTunnelId; + } else { + // Create the multi-tunnel + relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port); + relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; } + relayMultiTunnel.multiTunnelId = nextMultiTunnelId++; + + // Set the tunnel + relayMultiTunnels[userid + '/' + relayMultiTunnel.multiTunnelId] = relayMultiTunnel; + req.session.rid = relayMultiTunnel.multiTunnelId; + } // Redirect to root res.redirect('/');