From ab9b83b5f4526ce774f29d9bbfeac3cb0f9c68e4 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 25 Jun 2022 22:10:07 -0700 Subject: [PATCH] Added TLS support to web relay, #4172 --- apprelays.js | 110 +++++++++++++++++++++++++++------------ views/default.handlebars | 4 +- webrelayserver.js | 5 +- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/apprelays.js b/apprelays.js index 3046db38..8acd6fec 100644 --- a/apprelays.js +++ b/apprelays.js @@ -13,7 +13,6 @@ /*jshint esversion: 6 */ "use strict"; - /* Protocol numbers 10 = RDP @@ -59,9 +58,18 @@ const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576 const MESHRIGHT_ADMIN = 0xFFFFFFFF; +// SerialTunnel object is used to embed TLS within another connection. +function SerialTunnel(options) { + var obj = new require('stream').Duplex(options); + obj.forwardwrite = null; + obj.updateBuffer = function (chunk) { this.push(chunk); }; + obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); }; // Pass data written to forward + obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer() + return obj; +} // Construct a Web relay object -module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) { +module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid) { const obj = {}; obj.parent = parent; obj.lastOperation = Date.now(); @@ -70,6 +78,7 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us obj.nodeid = nodeid; obj.addr = addr; obj.port = port; + obj.appid = appid; var pendingRequests = []; var nextTunnelId = 1; var tunnels = {}; @@ -83,7 +92,6 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us // Handle new HTTP request obj.handleRequest = function (req, res) { - //console.log('handleRequest', req.url); pendingRequests.push([req, res]); handleNextRequest(); } @@ -96,7 +104,6 @@ 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'); const x = pendingRequests.shift(); tunnels[i].processRequest(x[0], x[1]); return; @@ -106,12 +113,18 @@ 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(obj, db, args, domain); - tunnel.onclose = function (tunnelId) { delete tunnels[tunnelId]; } + tunnel.onclose = function (tunnelId) { + delete tunnels[tunnelId]; + // Count how many non-websocket tunnels are active + var count = 0; + for (var i in tunnels) { count += (tunnels[i].isWebSocket ? 0 : 1); } + // If there are none, discard all pending HTTP requests + if (count == 0) { for (var i in pendingRequests) { const x = pendingRequests[i]; x[1].end(); pendingRequests = []; } } + } 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.connect(userid, nodeid, addr, port, appid); tunnel.tunnelId = nextTunnelId++; tunnels[tunnel.tunnelId] = tunnel; } @@ -131,7 +144,6 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us } - // Construct a Web relay object module.exports.CreateWebRelay = function (parent, db, args, domain) { //const Net = require('net'); @@ -141,6 +153,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { obj.relayActive = false; obj.closed = false; obj.isWebSocket = false; + const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. // Events obj.onclose = null; @@ -151,8 +164,6 @@ 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); - // Construct the HTTP request var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n'; request += 'host: ' + obj.addr + ':' + obj.port + '\r\n'; @@ -161,17 +172,14 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { 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; }); + req.on('end', function () { 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'); + send(Buffer.from(request)); } obj.res = res; } @@ -181,6 +189,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { if (obj.closed == true) return; obj.closed = true; + if (obj.tls) { + try { obj.tls.end(); } catch (ex) { console.log(ex); } + delete obj.tls; + } + /* // Event the session ending if ((obj.startTime) && (obj.meshid != null)) { @@ -215,10 +228,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { }; // Start the looppback server - obj.connect = function (userid, nodeid, addr, port) { + obj.connect = function (userid, nodeid, addr, port, appid) { if (obj.relayActive || obj.closed) return; obj.addr = addr; obj.port = port; + obj.appid = appid; // Encode a cookie for the mesh relay const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port }; @@ -236,21 +250,36 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { obj.wsClient = new WebSocket(url, options); 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 (obj.tls) { + // WS --> TLS + processRawHttpData(data); + } else if (obj.relayActive == false) { if ((data == 'c') || (data == 'cr')) { - obj.relayActive = true; - if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection + if (appid == 2) { + // TLS needs to be setup + obj.ser = new SerialTunnel(); + obj.ser.forwardwrite = function (data) { if (data.length > 0) { try { obj.wsClient.send(data); } catch (ex) { } } }; // TLS ---> WS + + // 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"); + obj.relayActive = true; + 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(); }); + + // Decrypted tunnel from TLS communcation to be forwarded to the browser + obj.tls.on('data', function (data) { processHttpData(data); }); // TLS ---> Browser + } else { + // No TLS needed, tunnel is now active + obj.relayActive = true; + if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection + } } } else { - if (typeof data == 'string') { - // Forward any ping/pong commands to the browser - var cmd = null; - try { cmd = JSON.parse(data); } catch (ex) { } - if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); } - return; - } - // Relay WS --> TCP, event data coming in - processHttpData(data.toString('binary')); + processRawHttpData(data); } }); obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); }); @@ -260,6 +289,23 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { } } + function processRawHttpData(data) { + if (typeof data == 'string') { + // Forward any ping/pong commands to the browser + var cmd = null; + try { cmd = JSON.parse(data); } catch (ex) { } + if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); } + return; + } + if (obj.tls) { + // If TLS is in use, WS --> TLS + if (data.length > 0) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } } + } else { + // Relay WS --> TCP, event data coming in + processHttpData(data.toString('binary')); + } + } + // Process incoming HTTP data obj.socketAccumulator = ''; obj.socketParseState = 0; @@ -335,12 +381,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) { if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); } } - // Send data thru the relay tunnel - function send(data) { - if (obj.relayActive = - false) return false; - obj.wsClient.send(data); - return true; - } + // 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'); return obj; diff --git a/views/default.handlebars b/views/default.handlebars index e6f688de..6152d254 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -4576,7 +4576,7 @@ if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) { if (webRelayPort != 0) { x += '' + "HTTP" + ' '; - //x += '' + "HTTPS" + ' '; + x += '' + "HTTPS" + ' '; } if ((node.agent.id > 0) && (node.agent.id < 5)) { if (navigator.platform.toLowerCase() == 'win32') { @@ -7148,7 +7148,7 @@ if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) { if (webRelayPort != 0) { x += '' + "HTTP" + ' '; - //x += '' + "HTTPS" + ' '; + x += '' + "HTTPS" + ' '; } if ((node.agent.id > 0) && (node.agent.id < 5)) { if (navigator.platform.toLowerCase() == 'win32') { diff --git a/webrelayserver.js b/webrelayserver.js index 7287be64..f8f5ef44 100644 --- a/webrelayserver.js +++ b/webrelayserver.js @@ -146,12 +146,13 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, 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 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)) { + if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port) && (xrelayMultiTunnel.appid == appid)) { relayMultiTunnel = xrelayMultiTunnel; // We found an exact match } } @@ -161,7 +162,7 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates, 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 = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port, appid); relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; } relayMultiTunnel.multiTunnelId = nextMultiTunnelId++;