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