Web relay improvements, #4240

Ylian Saint-Hilaire committed Jul 8, 2022 at 18:00 UTC 9dac8b780793781c4d0d421a2c89fee6899e9fc1
3 files changed +94 -79
apprelays.js
+24 -9
@@ -84,6 +84,8 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
84 var tunnels = {};
85 var errorCount = 0; // If we keep closing tunnels without processing requests, fail the requests
86
87 + parent.parent.debug('webrelay', 'CreateWebRelaySession, userid:' + userid + ', addr:' + addr + ', port:' + port);
88 +
89 // Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
90 obj.webCookies = {};
91
@@ -110,12 +112,14 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
112
113 // Handle new HTTP request
114 obj.handleRequest = function (req, res) {
115 + parent.parent.debug('webrelay', 'handleRequest, url:' + req.url);
116 pendingRequests.push([req, res, false]);
117 handleNextRequest();
118 }
119
120 // Handle new websocket request
121 obj.handleWebSocket = function (ws, req) {
122 + parent.parent.debug('webrelay', 'handleWebSocket, url:' + req.url);
123 pendingRequests.push([req, ws, true]);
124 handleNextRequest();
125 }
@@ -146,19 +150,26 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
150
151 function launchNewTunnel() {
152 // Launch a new tunnel
153 + parent.parent.debug('webrelay', 'launchNewTunnel');
154 const tunnel = module.exports.CreateWebRelay(obj, db, args, domain);
155 tunnel.onclose = function (tunnelId, processedCount) {
156 + if (tunnels == null) return;
157 + parent.parent.debug('webrelay', 'tunnel-onclose');
158 if (processedCount == 0) { errorCount++; } // If this tunnel closed without processing any requests, mark this as an error
159 delete tunnels[tunnelId];
160 handleNextRequest();
161 }
162 tunnel.onconnect = function (tunnelId) {
163 + if (tunnels == null) return;
164 + parent.parent.debug('webrelay', 'tunnel-onconnect');
165 if (pendingRequests.length > 0) {
166 const x = pendingRequests.shift();
167 if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
168 }
169 }
170 tunnel.oncompleted = function (tunnelId) {
171 + if (tunnels == null) return;
172 + parent.parent.debug('webrelay', 'tunnel-oncompleted');
173 errorCount = 0; // Something got completed, clear any error count
174 if (pendingRequests.length > 0) {
175 const x = pendingRequests.shift();
@@ -170,10 +181,14 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
181 tunnels[tunnel.tunnelId] = tunnel;
182 }
183
184 + // Close all tunnels
185 + obj.close = function () { close(); }
186 +
187 // Close all tunnels
188 function close() {
189 // Set the session as closed
190 if (obj.closed == true) return;
191 + parent.parent.debug('webrelay', 'tunnel-close');
192 obj.closed = true;
193
194 // Close all tunnels
@@ -291,7 +306,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
306
307 function sendWebSocketFrameToDevice(op, payload) {
308 // Select a random mask
294 - const mask = parent.parent.crypto.randomBytes(4)
309 + const mask = parent.parent.parent.crypto.randomBytes(4)
310
311 // Setup header and mask
312 var header = null;
@@ -376,7 +391,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
391 // Encode a cookie for the mesh relay
392 const cookieContent = { userid: userid, domainid: domain.id, nodeid: nodeid, tcpport: port };
393 if (addr != null) { cookieContent.tcpaddr = addr; }
379 - const cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
394 + const cookie = parent.parent.parent.encodeCookie(cookieContent, parent.parent.parent.loginCookieEncryptionKey);
395
396 try {
397 // Setup the correct URL with domain and use TLS only if needed.
@@ -385,9 +400,9 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
400 var domainadd = '';
401 if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
402 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
388 - parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
403 + parent.parent.parent.debug('relay', 'TCP: Connection websocket to ' + url);
404 obj.wsClient = new WebSocket(url, options);
390 - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'TCP: Relay websocket open'); });
405 + obj.wsClient.on('open', function () { parent.parent.parent.debug('relay', 'TCP: Relay websocket open'); });
406 obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
407 if (obj.tls) {
408 // WS --> TLS
@@ -402,13 +417,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
417 // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel
418 const tlsoptions = { socket: obj.ser, rejectUnauthorized: false };
419 obj.tls = require('tls').connect(tlsoptions, function () {
405 - parent.parent.debug('relay', "Web Relay Secure TLS Connection");
420 + parent.parent.parent.debug('relay', "Web Relay Secure TLS Connection");
421 obj.relayActive = true;
422 parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
423 if (obj.onconnect) { obj.onconnect(obj.tunnelId); } // Event connection
424 });
425 obj.tls.setEncoding('binary');
411 - obj.tls.on('error', function (err) { parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); });
426 + obj.tls.on('error', function (err) { parent.parent.parent.debug('relay', "Web Relay TLS Connection Error", err); obj.close(); });
427
428 // Decrypted tunnel from TLS communcation to be forwarded to the browser
429 obj.tls.on('data', function (data) { processHttpData(data); }); // TLS ---> Browser
@@ -423,8 +438,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
438 processRawHttpData(data);
439 }
440 });
426 - obj.wsClient.on('close', function () { parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
427 - obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
441 + obj.wsClient.on('close', function () { parent.parent.parent.debug('relay', 'TCP: Relay websocket closed'); obj.close(); });
442 + obj.wsClient.on('error', function (err) { parent.parent.parent.debug('relay', 'TCP: Relay websocket error: ' + err); obj.close(); });
443 } catch (ex) {
444 console.log(ex);
445 }
@@ -656,7 +671,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
671 // Send data thru the relay tunnel. Written to use TLS if needed.
672 function send(data) { try { if (obj.tls) { obj.tls.write(data); } else { obj.wsClient.send(data); } } catch (ex) { } }
673
659 - parent.parent.debug('relay', 'TCP: Request for web relay');
674 + parent.parent.parent.debug('relay', 'TCP: Request for web relay');
675 return obj;
676 };
677
webrelayserver.js
+29 -35
@@ -29,7 +29,6 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
29 obj.webRelayServer = null;
30 obj.port = 0;
31 obj.cleanupTimer = null;
32 - var nextSessionId = 1;
32 var relaySessions = {} // RelayID --> Web Mutli-Tunnel
33 const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
34 var tlsSessionStore = {}; // Store TLS session information for quick resume.
@@ -68,14 +67,14 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
67 httpOnly: true,
68 keys: [args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
69 secure: (args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
71 - sameSite: args.sessionsamesite
70 + sameSite: (args.sessionsamesite ? args.sessionsamesite : 'lax')
71 }
72 if (args.sessiontime != null) { sessionOptions.maxAge = (args.sessiontime * 60 * 1000); }
73 obj.app.use(obj.session(sessionOptions));
74
75 // Add HTTP security headers to all responses
76 obj.app.use(function (req, res, next) {
78 - parent.debug('webrequest', req.url + ' (RelayServer)');
77 + parent.debug('webrelay', req.url);
78 res.removeHeader('X-Powered-By');
79 res.set({
80 'strict-transport-security': 'max-age=60000; includeSubDomains',
@@ -83,7 +82,7 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
82 'x-frame-options': 'SAMEORIGIN',
83 'X-XSS-Protection': '1; mode=block',
84 'X-Content-Type-Options': 'nosniff',
86 - 'Content-Security-Policy': "default-src 'none'; style-src 'self' 'unsafe-inline';"
85 + 'Content-Security-Policy': "default-src 'self'; style-src 'self' 'unsafe-inline';"
86 });
87
88 // Set the real IP address of the request
@@ -121,8 +120,8 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
120 return next();
121 } else {
122 // If this is a normal request (GET, POST, etc) handle it here
124 - if ((req.session.userid != null) && (req.session.rid != null)) {
125 - var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
123 + if ((req.session.userid != null) && (req.session.x != null)) {
124 + var relaySession = relaySessions[req.session.userid + '/' + req.session.x];
125 if (relaySession != null) {
126 // The web relay session is valid, use it
127 relaySession.handleRequest(req, res);
@@ -154,8 +153,8 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
153
154 // Handle incoming web socket calls
155 obj.app.ws('/*', function (ws, req) {
157 - if ((req.session.userid != null) && (req.session.rid != null)) {
158 - var relaySession = relaySessions[req.session.userid + '/' + req.session.rid];
156 + if ((req.session.userid != null) && (req.session.x != null)) {
157 + var relaySession = relaySessions[req.session.userid + '/' + req.session.x];
158 if (relaySession != null) {
159 // The multi-tunnel session is valid, use it
160 relaySession.handleWebSocket(ws, req);
@@ -173,10 +172,10 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
172 obj.app.get('/control-redirect.ashx', function (req, res) {
173 if ((req.session == null) || (req.session.userid == null)) { res.redirect('/'); return; }
174 res.set({ 'Cache-Control': 'no-store' });
176 - parent.debug('web', 'webRelaySetup');
175 + parent.debug('webrelay', 'webRelaySetup');
176
177 // Check that all the required arguments are present
179 - if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
178 + 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; }
179
180 // Get the user and domain information
181 const userid = req.session.userid;
@@ -188,36 +187,31 @@ module.exports.CreateWebRelayServer = function (parent, db, args, certificates,
187 const appid = parseInt(req.query.appid);
188
189 // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
191 - var relaySession = null;
192 - for (var i in relaySessions) {
193 - const xrelaySession = relaySessions[i];
194 - if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
195 - relaySession = xrelaySession; // We found an exact match
196 - }
190 + const xrelaySession = relaySessions[req.session.userid + '/' + req.session.x];
191 + if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
192 + // We found an exact match, we are all setup already, redirect to root
193 + res.redirect('/');
194 + return;
195 }
196
199 - if (relaySession != null) {
200 - // Since we found a match, use it
201 - req.session.rid = relaySession.sessionId;
202 - } else {
203 - // Create a web relay session
204 - relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
205 - relaySession.onclose = function (sessionId) {
206 - // Remove the relay session
207 - delete relaySessions[sessionId];
208 - // If there are not more relay sessions, clear the cleanup timer
209 - if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; }
210 - }
211 - relaySession.sessionId = nextSessionId++;
212 -
213 - // Set the multi-tunnel session
214 - relaySessions[userid + '/' + relaySession.sessionId] = relaySession;
215 - req.session.rid = relaySession.sessionId;
197 + // There is a relay session, but it's not correct, close it.
198 + if (xrelaySession != null) { xrelaySession.close(); delete relaySessions[req.session.userid + '/' + req.session.x]; }
199
217 - // Setup the cleanup timer if needed
218 - if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); }
200 + // Create a web relay session
201 + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid);
202 + relaySession.onclose = function (sessionId) {
203 + // Remove the relay session
204 + delete relaySessions[sessionId];
205 + // If there are not more relay sessions, clear the cleanup timer
206 + if ((Object.keys(relaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(obj.cleanupTimer); obj.cleanupTimer = null; }
207 }
208
209 + // Set the multi-tunnel session
210 + relaySessions[userid + '/' + req.session.x] = relaySession;
211 +
212 + // Setup the cleanup timer if needed
213 + if (obj.cleanupTimer == null) { obj.cleanupTimer = setInterval(checkTimeout, 10000); }
214 +
215 // Redirect to root
216 res.redirect('/');
217 });
webserver.js
+41 -35
@@ -5758,11 +5758,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5758 }
5759 }
5760 }
5761 - //obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
5761 +
5762 + // Setup the cookie session
5763 var sessionOptions = {
5764 name: 'xid', // Recommended security practice to not use the default cookie name
5765 httpOnly: true,
5765 - domain: (certificates.CommonName != 'un-configured' ? "." + certificates.CommonName : null),
5766 keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
5767 secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5768 sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax')
@@ -5876,7 +5876,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5876 }
5877
5878 // If this is a web relay connection, handle it here.
5879 - if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) { return obj.webRelayRouter(req, res); }
5879 + if ((obj.webRelayRouter != null) && (req.hostname == obj.args.relaydns)) {
5880 + if (['GET', 'POST', 'PUT', 'HEAD'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; }
5881 + }
5882
5883 // Get the domain for this request
5884 const domain = req.xdomain = getDomain(req);
@@ -6590,7 +6592,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6592 parent.debug('web', 'webRelaySetup');
6593
6594 // Check that all the required arguments are present
6593 - if ((req.session.userid == null) || (req.query.n == null) || (req.query.p == null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
6595 + 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; }
6596
6597 // Get the user and domain information
6598 const userid = req.session.userid;
@@ -6602,45 +6604,49 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6604 const appid = parseInt(req.query.appid);
6605
6606 // Check to see if we already have a multi-relay session that matches exactly this device and port for this user
6605 - var relaySession = null;
6606 - for (var i in webRelaySessions) {
6607 - const xrelaySession = webRelaySessions[i];
6608 - if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6609 - relaySession = xrelaySession; // We found an exact match
6610 - }
6607 + const xrelaySession = webRelaySessions[req.session.userid + '/' + req.session.x];
6608 + if ((xrelaySession != null) && (xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6609 + // We found an exact match, we are all setup already, redirect to root
6610 + res.redirect('/');
6611 + return;
6612 }
6613
6613 - if (relaySession != null) {
6614 - // Since we found a match, use it
6615 - req.session.rid = relaySession.sessionId;
6616 - } else {
6617 - // Create a web relay session
6618 - relaySession = require('./apprelays.js').CreateWebRelaySession(parent, db, req, args, domain, userid, nodeid, addr, port, appid);
6619 - relaySession.onclose = function (sessionId) {
6620 - // Remove the relay session
6621 - delete webRelaySessions[sessionId];
6622 - // If there are not more relay sessions, clear the cleanup timer
6623 - if ((Object.keys(webRelaySessions).length == 0) && (webRelayCleanupTimer != null)) { clearInterval(webRelayCleanupTimer); webRelayCleanupTimer = null; }
6624 - }
6625 - relaySession.sessionId = webRelayNextSessionId++;
6626 -
6627 - // Set the multi-tunnel session
6628 - webRelaySessions[userid + '/' + relaySession.sessionId] = relaySession;
6629 - req.session.rid = relaySession.sessionId;
6614 + // There is a relay session, but it's not correct, close it.
6615 + if (xrelaySession != null) {
6616 + xrelaySession.close();
6617 + delete webRelaySessions[req.session.userid + '/' + req.session.x];
6618 + }
6619
6631 - // Setup the cleanup timer if needed
6632 - if (webRelayCleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6620 + // Create a web relay session
6621 + const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid);
6622 + relaySession.onclose = function (sessionId) {
6623 + // Remove the relay session
6624 + delete webRelaySessions[sessionId];
6625 + // If there are not more relay sessions, clear the cleanup timer
6626 + if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
6627 }
6628
6629 + // Set the multi-tunnel session
6630 + webRelaySessions[userid + '/' + req.session.x] = relaySession;
6631 +
6632 + // Setup the cleanup timer if needed
6633 + if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6634 +
6635 // Redirect to root
6636 res.redirect('/');
6637 });
6638
6639 // Handle all incoming requests as web relays
6640 - obj.webRelayRouter.get('/*', function (req, res) { handleWebRelayRequest(req, res); })
6640 + obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6641 +
6642 + // Handle all incoming requests as web relays
6643 + obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6644 +
6645 + // Handle all incoming requests as web relays
6646 + obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6647
6648 // Handle all incoming requests as web relays
6643 - obj.webRelayRouter.post('/*', function (req, res) { handleWebRelayRequest(req, res); })
6649 + obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
6650 }
6651
6652 // 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
6691
6692 // Handle an incoming request as a web relay
6693 function handleWebRelayRequest(req, res) {
6688 - if ((req.session.userid != null) && (req.session.rid != null)) {
6689 - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
6694 + if ((req.session.userid != null) && (req.session.x != null)) {
6695 + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x];
6696 if (relaySession != null) {
6697 // The web relay session is valid, use it
6698 relaySession.handleRequest(req, res);
@@ -6702,8 +6708,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6708
6709 // Handle an incoming websocket connection as a web relay
6710 function handleWebRelayWebSocket(ws, req) {
6705 - if ((req.session.userid != null) && (req.session.rid != null)) {
6706 - var relaySession = webRelaySessions[req.session.userid + '/' + req.session.rid];
6711 + if ((req.session.userid != null) && (req.session.x != null)) {
6712 + var relaySession = webRelaySessions[req.session.userid + '/' + req.session.x];
6713 if (relaySession != null) {
6714 // The multi-tunnel session is valid, use it
6715 relaySession.handleWebSocket(ws, req);