Added TLS support to web relay, #4172

Ylian Saint-Hilaire committed Jun 25, 2022 at 22:10 UTC ab9b83b5f4526ce774f29d9bbfeac3cb0f9c68e4
3 files changed +81 -38
apprelays.js
+76 -34
@@ -13,7 +13,6 @@
13 /*jshint esversion: 6 */
14 "use strict";
15
16 -
16 /*
17 Protocol numbers
18 10 = RDP
@@ -59,9 +58,18 @@ const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
58 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
59 const MESHRIGHT_ADMIN = 0xFFFFFFFF;
60
61 +// SerialTunnel object is used to embed TLS within another connection.
62 +function SerialTunnel(options) {
63 + var obj = new require('stream').Duplex(options);
64 + obj.forwardwrite = null;
65 + obj.updateBuffer = function (chunk) { this.push(chunk); };
66 + 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
67 + obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
68 + return obj;
69 +}
70
71 // Construct a Web relay object
64 -module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port) {
72 +module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, userid, nodeid, addr, port, appid) {
73 const obj = {};
74 obj.parent = parent;
75 obj.lastOperation = Date.now();
@@ -70,6 +78,7 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
78 obj.nodeid = nodeid;
79 obj.addr = addr;
80 obj.port = port;
81 + obj.appid = appid;
82 var pendingRequests = [];
83 var nextTunnelId = 1;
84 var tunnels = {};
@@ -83,7 +92,6 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
92
93 // Handle new HTTP request
94 obj.handleRequest = function (req, res) {
86 - //console.log('handleRequest', req.url);
95 pendingRequests.push([req, res]);
96 handleNextRequest();
97 }
@@ -96,7 +104,6 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
104 count += (tunnels[i].isWebSocket ? 0 : 1);
105 if ((tunnels[i].relayActive == true) && (tunnels[i].res == null)) {
106 // Found a free tunnel, use it
99 - //console.log('handleNextRequest-found empty tunnel');
107 const x = pendingRequests.shift();
108 tunnels[i].processRequest(x[0], x[1]);
109 return;
@@ -106,12 +113,18 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
113 if (count > 0) return;
114
115 // Launch a new tunnel
109 - //console.log('handleNextRequest-starting new tunnel');
116 const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
111 - tunnel.onclose = function (tunnelId) { delete tunnels[tunnelId]; }
117 + tunnel.onclose = function (tunnelId) {
118 + delete tunnels[tunnelId];
119 + // Count how many non-websocket tunnels are active
120 + var count = 0;
121 + for (var i in tunnels) { count += (tunnels[i].isWebSocket ? 0 : 1); }
122 + // If there are none, discard all pending HTTP requests
123 + if (count == 0) { for (var i in pendingRequests) { const x = pendingRequests[i]; x[1].end(); pendingRequests = []; } }
124 + }
125 tunnel.onconnect = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
126 tunnel.oncompleted = function (tunnelId) { if (pendingRequests.length > 0) { const x = pendingRequests.shift(); tunnels[tunnelId].processRequest(x[0], x[1]); } }
114 - tunnel.connect(userid, nodeid, addr, port);
127 + tunnel.connect(userid, nodeid, addr, port, appid);
128 tunnel.tunnelId = nextTunnelId++;
129 tunnels[tunnel.tunnelId] = tunnel;
130 }
@@ -131,7 +144,6 @@ module.exports.CreateMultiWebRelay = function (parent, db, req, args, domain, us
144 }
145
146
134 -
147 // Construct a Web relay object
148 module.exports.CreateWebRelay = function (parent, db, args, domain) {
149 //const Net = require('net');
@@ -141,6 +153,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
153 obj.relayActive = false;
154 obj.closed = false;
155 obj.isWebSocket = false;
156 + const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
157
158 // Events
159 obj.onclose = null;
@@ -151,8 +164,6 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
164 obj.processRequest = function (req, res) {
165 if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
166
154 - //console.log('processRequest-start', req.method);
155 -
167 // Construct the HTTP request
168 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
169 request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
@@ -161,17 +172,14 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
172 if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
173 request += '\r\n';
174
164 - //console.log('request', request);
165 -
175 if ((req.headers['transfer-encoding'] != null) || (req.headers['content-length'] != null)) {
176 // Read the HTTP body and send the request to the device
177 obj.requestBinary = [Buffer.from(request)];
178 req.on('data', function (data) { obj.requestBinary.push(data); });
170 - req.on('end', function () { obj.wsClient.send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; });
179 + req.on('end', function () { send(Buffer.concat(obj.requestBinary)); delete obj.requestBinary; });
180 } else {
181 // Request has no body, send it now
173 - obj.wsClient.send(Buffer.from(request));
174 - //console.log('processRequest-sent-nobody');
182 + send(Buffer.from(request));
183 }
184 obj.res = res;
185 }
@@ -181,6 +189,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
189 if (obj.closed == true) return;
190 obj.closed = true;
191
192 + if (obj.tls) {
193 + try { obj.tls.end(); } catch (ex) { console.log(ex); }
194 + delete obj.tls;
195 + }
196 +
197 /*
198 // Event the session ending
199 if ((obj.startTime) && (obj.meshid != null)) {
@@ -215,10 +228,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
228 };
229
230 // Start the looppback server
218 - obj.connect = function (userid, nodeid, addr, port) {
231 + obj.connect = function (userid, nodeid, addr, port, appid) {
232 if (obj.relayActive || obj.closed) return;
233 obj.addr = addr;
234 obj.port = port;
235 + obj.appid = appid;
236
237 // Encode a cookie for the mesh relay
238 const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
@@ -236,21 +250,36 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
250 obj.wsClient = new WebSocket(url, options);
251 obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
252 obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
239 - if (obj.relayActive == false) {
253 + if (obj.tls) {
254 + // WS --> TLS
255 + processRawHttpData(data);
256 + } else if (obj.relayActive == false) {
257 if ((data == 'c') || (data == 'cr')) {
241 - obj.relayActive = true;
242 - if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
258 + if (appid == 2) {
259 + // TLS needs to be setup
260 + obj.ser = new SerialTunnel();
261 + obj.ser.forwardwrite = function (data) { if (data.length > 0) { try { obj.wsClient.send(data); } catch (ex) { } } }; // TLS ---> WS
262 +
263 + // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel
264 + const tlsoptions = { socket: obj.ser, rejectUnauthorized: false };
265 + obj.tls = require('tls').connect(tlsoptions, function () {
266 + parent.parent.debug('relay', "Web Relay Secure TLS Connection");
267 + obj.relayActive = true;
268 + if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
269 + });
270 + obj.tls.setEncoding('binary');
271 + obj.tls.on('error', function (err) { parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); });
272 +
273 + // Decrypted tunnel from TLS communcation to be forwarded to the browser
274 + obj.tls.on('data', function (data) { processHttpData(data); }); // TLS ---> Browser
275 + } else {
276 + // No TLS needed, tunnel is now active
277 + obj.relayActive = true;
278 + if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
279 + }
280 }
281 } else {
245 - if (typeof data == 'string') {
246 - // Forward any ping/pong commands to the browser
247 - var cmd = null;
248 - try { cmd = JSON.parse(data); } catch (ex) { }
249 - if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); }
250 - return;
251 - }
252 - // Relay WS --> TCP, event data coming in
253 - processHttpData(data.toString('binary'));
282 + processRawHttpData(data);
283 }
284 });
285 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) {
289 }
290 }
291
292 + function processRawHttpData(data) {
293 + if (typeof data == 'string') {
294 + // Forward any ping/pong commands to the browser
295 + var cmd = null;
296 + try { cmd = JSON.parse(data); } catch (ex) { }
297 + if ((cmd != null) && (cmd.ctrlChannel == '102938') && (cmd.type == 'ping')) { cmd.type = 'pong'; obj.wsClient.send(JSON.stringify(cmd)); }
298 + return;
299 + }
300 + if (obj.tls) {
301 + // If TLS is in use, WS --> TLS
302 + if (data.length > 0) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } }
303 + } else {
304 + // Relay WS --> TCP, event data coming in
305 + processHttpData(data.toString('binary'));
306 + }
307 + }
308 +
309 // Process incoming HTTP data
310 obj.socketAccumulator = '';
311 obj.socketParseState = 0;
@@ -335,12 +381,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
381 if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
382 }
383
338 - // Send data thru the relay tunnel
339 - function send(data) {
340 - if (obj.relayActive = - false) return false;
341 - obj.wsClient.send(data);
342 - return true;
343 - }
384 + // Send data thru the relay tunnel. Written to use TLS if needed.
385 + function send(data) { try { if (obj.tls) { obj.tls.write(data); } else { obj.wsClient.send(data); } } catch (ex) { } }
386
387 parent.parent.debug('relay', 'TCP: Request for web relay');
388 return obj;
views/default.handlebars
+2 -2
@@ -4576,7 +4576,7 @@
4576 if ((((node.conn & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && (node.agent.id != 14)) {
4577 if (webRelayPort != 0) {
4578 x += '<a href=# onclick=p10WebRouter("' + node._id + '",1,80)>' + "HTTP" + '</a>&nbsp;';
4579 - //x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a>&nbsp;';
4579 + x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a>&nbsp;';
4580 }
4581 if ((node.agent.id > 0) && (node.agent.id < 5)) {
4582 if (navigator.platform.toLowerCase() == 'win32') {
@@ -7148,7 +7148,7 @@
7148 if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0)) {
7149 if (webRelayPort != 0) {
7150 x += '<a href=# onclick=p10WebRouter("' + node._id + '",1,80)>' + "HTTP" + '</a>&nbsp;';
7151 - //x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a>&nbsp;';
7151 + x += '<a href=# onclick=p10WebRouter("' + node._id + '",2,443)>' + "HTTPS" + '</a>&nbsp;';
7152 }
7153 if ((node.agent.id > 0) && (node.agent.id < 5)) {
7154 if (navigator.platform.toLowerCase() == 'win32') {
webrelayserver.js
+3 -2
@@ -146,12 +146,13 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
146 const nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
147 const addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
148 const port = parseInt(req.query.p);
149 + const appid = parseInt(req.query.appid);
150
151 // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
152 var relayMultiTunnel = null;
153 for (var i in relayMultiTunnels) {
154 const xrelayMultiTunnel = relayMultiTunnels[i];
154 - if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port)) {
155 + if ((xrelayMultiTunnel.domain.id == domain.id) && (xrelayMultiTunnel.userid == userid) && (xrelayMultiTunnel.nodeid == nodeid) && (xrelayMultiTunnel.addr == addr) && (xrelayMultiTunnel.port == port) && (xrelayMultiTunnel.appid == appid)) {
156 relayMultiTunnel = xrelayMultiTunnel; // We found an exact match
157 }
158 }
@@ -161,7 +162,7 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
162 req.session.rid = relayMultiTunnel.multiTunnelId;
163 } else {
164 // Create the multi-tunnel
164 - relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port);
165 + relayMultiTunnel = require('./apprelays.js').CreateMultiWebRelay(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
166 relayMultiTunnel.onclose = function (multiTunnelId) { delete obj.relayTunnels[multiTunnelId]; }
167 relayMultiTunnel.multiTunnelId = nextMultiTunnelId++;
168