Web relay can now handle connection:close responses.
Ylian Saint-Hilaire committed
Jul 10, 2022 at 13:08 UTC
a151dcbfe6a7f3e5bd5374aa47c54fce43f97ac6
2 files changed
+104
-75
apprelays.js
+48
-20
@@ -168,13 +168,19 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
168
if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
169
}
170
}
171
- tunnel.oncompleted = function (tunnelId) {
171
+ tunnel.oncompleted = function (tunnelId, closed) {
172
if (tunnels == null) return;
173
- parent.parent.debug('webrelay', 'tunnel-oncompleted');
174
- errorCount = 0; // Something got completed, clear any error count
175
- if (pendingRequests.length > 0) {
176
- const x = pendingRequests.shift();
177
- if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
173
+ if (closed === true) {
174
+ parent.parent.debug('webrelay', 'tunnel-oncompleted and closed');
175
+ } else {
176
+ parent.parent.debug('webrelay', 'tunnel-oncompleted');
177
+ }
178
+ if (closed !== true) {
179
+ errorCount = 0; // Something got completed, clear any error count
180
+ if (pendingRequests.length > 0) {
181
+ const x = pendingRequests.shift();
182
+ if (x[2] == true) { tunnels[tunnelId].processWebSocket(x[0], x[1]); } else { tunnels[tunnelId].processRequest(x[0], x[1]); }
183
+ }
184
}
185
}
186
tunnel.connect(userid, nodeid, addr, port, appid);
@@ -343,6 +349,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
349
if (obj.closed == true) return;
350
obj.closed = true;
351
352
+ // If we are processing a http response that terminates when it closes, do this now.
353
+ if ((obj.socketParseState == 1) && (obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) {
354
+ processHttpResponse(null, obj.socketAccumulator, true, true); // Indicate this tunnel is done and also closed, do not put a new request on this tunnel.
355
+ obj.socketAccumulator = '';
356
+ obj.socketParseState = 0;
357
+ }
358
+
359
if (obj.tls) {
360
try { obj.tls.end(); } catch (ex) { console.log(ex); }
361
delete obj.tls;
@@ -468,6 +481,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
481
obj.socketParseState = 0;
482
obj.socketContentLengthRemaining = 0;
483
function processHttpData(data) {
484
+ //console.log('processHttpData', data.length);
485
obj.socketAccumulator += data;
486
while (true) {
487
//console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator);
@@ -492,8 +506,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
506
}
507
508
// Check if this HTTP request has a body
495
- if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) { obj.socketParseState = 1; }
509
if (obj.socketXHeader['content-length'] != null) { obj.socketParseState = 1; }
510
+ if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) { obj.socketParseState = 1; }
511
if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) { obj.socketParseState = 1; }
512
if (obj.isWebSocket) {
513
if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'upgrade')) {
@@ -510,21 +524,35 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
524
}
525
if (obj.socketParseState == 1) {
526
var csize = -1;
513
- if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) {
514
- // The body ends with a close, in this case, we will only process the header
515
- processHttpResponse(null, null, true);
516
- csize = 0;
517
- } else if (obj.socketXHeader['content-length'] != null) {
527
+ if (obj.socketXHeader['content-length'] != null) {
528
// The body length is specified by the content-length
529
if (obj.socketContentLengthRemaining == 0) { obj.socketContentLengthRemaining = parseInt(obj.socketXHeader['content-length']); } // Set the remaining content-length if not set
530
var data = obj.socketAccumulator.substring(0, obj.socketContentLengthRemaining); // Grab the available data, not passed the expected content-length
531
obj.socketAccumulator = obj.socketAccumulator.substring(data.length); // Remove the data from the accumulator
532
obj.socketContentLengthRemaining -= data.length; // Substract the obtained data from the expected size
523
- processHttpResponse(null, data, (obj.socketContentLengthRemaining == 0)); // Send any data we have, if we are done, signal the end of the response
524
- if (obj.socketContentLengthRemaining > 0) return; // If more data is needed, return now so we exit the while() loop.
533
+ if (obj.socketContentLengthRemaining > 0) {
534
+ // Send any data we have, if we are done, signal the end of the response
535
+ processHttpResponse(null, data, false);
536
+ return; // More data is needed, return now so we exit the while() loop.
537
+ } else {
538
+ // We are done with this request
539
+ const closing = (obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close');
540
+ if (closing) {
541
+ // We need to close this tunnel.
542
+ processHttpResponse(null, data, false);
543
+ obj.close();
544
+ } else {
545
+ // Proceed with the next request.
546
+ processHttpResponse(null, data, true);
547
+ }
548
+ }
549
csize = 0; // We are done
526
- }
527
- else if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) {
550
+ } else if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) {
551
+ // The body ends with a close, in this case, we will only process the header
552
+ processHttpResponse(null, obj.socketAccumulator, false);
553
+ obj.socketAccumulator = '';
554
+ return;
555
+ } else if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) {
556
// The body is chunked
557
var clen = obj.socketAccumulator.indexOf('\r\n');
558
if (clen < 0) { return; } // Chunk length not found, exit now and get more data.
@@ -602,8 +630,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
630
}
631
632
// This is a fully parsed HTTP response from the remote device
605
- function processHttpResponse(header, data, done) {
606
- //console.log('processHttpResponse');
633
+ function processHttpResponse(header, data, done, closed) {
634
+ //console.log('processHttpResponse', header, data ? data.length : 0, done, closed);
635
if (obj.isWebSocket == false) {
636
if (obj.res == null) return;
637
parent.lastOperation = obj.lastOperation = Date.now(); // Update time of last opertion performed
@@ -611,7 +639,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
639
// If there is a header, send it
640
if (header != null) {
641
obj.res.status(parseInt(header.Directive[1])); // Set the status
614
- const blockHeaders = ['Directive', 'sec-websocket-extensions']; // We do not forward these headers
642
+ const blockHeaders = ['Directive', 'sec-websocket-extensions', 'connection', 'transfer-encoding']; // We do not forward these headers
643
for (var i in header) {
644
if (i == 'set-cookie') {
645
for (var ii in header[i]) {
@@ -653,7 +681,7 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
681
682
// Event completion
683
obj.processedRequestCount++;
656
- if (obj.oncompleted) { obj.oncompleted(obj.tunnelId); }
684
+ if (obj.oncompleted) { obj.oncompleted(obj.tunnelId, closed); }
685
}
686
} else {
687
// Tunnel is now in web socket pass-thru mode
webserver.js
+56
-55
@@ -6612,61 +6612,60 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6612
const port = parseInt(req.query.p);
6613
const appid = parseInt(req.query.appid);
6614
6615
- try {
6616
-
6617
- // Check that we have an exact session on any of the relay DNS names
6618
- var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
6619
- for (var hostIndex in obj.args.relaydns) {
6620
- const host = obj.args.relaydns[hostIndex];
6621
- xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + host;
6622
- xrelaySession = webRelaySessions[xrelaySessionId];
6623
- if (xrelaySession == null) {
6624
- // We found an unused hostname, save this as it could be useful.
6625
- if (freeRelayHost == null) { freeRelayHost = host; }
6626
- } else {
6627
- // Check if we already have a relay session that matches exactly what we want
6628
- if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6629
- // We found an exact match, we are all setup already, redirect to root of that DNS name
6630
- if (host == req.hostname) {
6631
- // Request was made on the same host, redirect to root.
6632
- res.redirect('/');
6633
- } else {
6634
- // Request was made to a different host
6635
- const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6636
- res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
6637
- }
6638
- return;
6615
+ // Check that we have an exact session on any of the relay DNS names
6616
+ var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
6617
+ for (var hostIndex in obj.args.relaydns) {
6618
+ const host = obj.args.relaydns[hostIndex];
6619
+ xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + host;
6620
+ xrelaySession = webRelaySessions[xrelaySessionId];
6621
+ if (xrelaySession == null) {
6622
+ // We found an unused hostname, save this as it could be useful.
6623
+ if (freeRelayHost == null) { freeRelayHost = host; }
6624
+ } else {
6625
+ // Check if we already have a relay session that matches exactly what we want
6626
+ if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
6627
+ // We found an exact match, we are all setup already, redirect to root of that DNS name
6628
+ if (host == req.hostname) {
6629
+ // Request was made on the same host, redirect to root.
6630
+ res.redirect('/');
6631
+ } else {
6632
+ // Request was made to a different host
6633
+ const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6634
+ res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
6635
}
6636
+ return;
6637
+ }
6638
6641
- // Keep a record of the oldest web relay session, this could be useful.
6642
- if (oldestRelayHost == null) {
6643
- // Oldest host not set yet, set it
6639
+ // Keep a record of the oldest web relay session, this could be useful.
6640
+ if (oldestRelayHost == null) {
6641
+ // Oldest host not set yet, set it
6642
+ oldestRelayHost = host;
6643
+ oldestRelayTime = xrelaySession.lastOperation;
6644
+ } else {
6645
+ // Check if this host is older then oldest so far
6646
+ if (oldestRelayTime > xrelaySession.lastOperation) {
6647
oldestRelayHost = host;
6648
oldestRelayTime = xrelaySession.lastOperation;
6646
- } else {
6647
- // Check if this host is older then oldest so far
6648
- if (oldestRelayTime > xrelaySession.lastOperation) {
6649
- oldestRelayHost = host;
6650
- oldestRelayTime = xrelaySession.lastOperation;
6651
- }
6649
}
6650
}
6651
}
6652
+ }
6653
6656
- // Check if there is a free relay DNS name we can use
6657
- var selectedHost = null;
6658
- if (freeRelayHost != null) {
6659
- // There is a free one, use it.
6660
- selectedHost = freeRelayHost;
6661
- xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + selectedHost;
6662
- } else {
6663
- // No free ones, close the oldest one
6664
- selectedHost = oldestRelayHost;
6665
- xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + selectedHost;
6666
- xrelaySession = webRelaySessions[xrelaySessionId];
6667
- xrelaySession.close();
6668
- delete webRelaySessions[xrelaySessionId];
6669
- }
6654
+ // Check if there is a free relay DNS name we can use
6655
+ var selectedHost = null;
6656
+ if (freeRelayHost != null) {
6657
+ // There is a free one, use it.
6658
+ selectedHost = freeRelayHost;
6659
+ } else {
6660
+ // No free ones, close the oldest one
6661
+ selectedHost = oldestRelayHost;
6662
+ }
6663
+ xrelaySessionId = req.session.userid + '/' + req.session.x + '/' + selectedHost;
6664
+
6665
+ if (selectedHost == req.hostname) {
6666
+ // If this web relay session id is not free, close it now
6667
+ xrelaySession = webRelaySessions[xrelaySessionId];
6668
+ if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
6669
6670
// Create a web relay session
6671
const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId);
@@ -6683,16 +6682,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6682
// Setup the cleanup timer if needed
6683
if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
6684
6686
- if (selectedHost == req.hostname) {
6687
- // Request was made on the same host, redirect to root.
6688
- res.redirect('/');
6685
+ // Redirect to root.
6686
+ res.redirect('/');
6687
+ } else {
6688
+ if (req.query.noredirect != null) {
6689
+ // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
6690
+ res.sendStatus(404);
6691
} else {
6690
- // Request was made to a different host
6692
+ // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name
6693
const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
6692
- res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + '/');
6694
+ res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
6695
}
6694
-
6695
- } catch (ex) { console.log(ex); }
6696
+ }
6697
});
6698
6699
// Handle all incoming requests as web relays
@@ -6716,7 +6717,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6717
6718
// Indicates to ExpressJS that the override public folder should be used to serve static files.
6719
if (parent.config.domains[i].webpublicpath != null) {
6719
- // Use domain public path
6720
+ // Use domain public pathe
6721
obj.app.use(url, obj.express.static(parent.config.domains[i].webpublicpath));
6722
} else if (obj.parent.webPublicOverridePath != null) {
6723
// Use override path