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);