Improved web relay cookie handling, #4172

Ylian Saint-Hilaire committed Jun 29, 2022 at 14:16 UTC 22edf8d45f8f7b0704239639592be6eb87a75ab9
1 file changed +41 -13
apprelays.js
+41 -13
@@ -84,7 +84,7 @@ module.exports.CreateWebRelaySession = function (parent, db, req, args, domain,
84 var tunnels = {};
85
86 // Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
87 - obj.webCookie = null;
87 + obj.webCookies = {};
88
89 // Events
90 obj.closed = false;
@@ -203,10 +203,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
203
204 // Construct the HTTP request
205 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
206 - request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
207 - const blockedHeaders = ['origin', 'host', 'cookie']; // These are headers we do not forward
206 + const blockedHeaders = ['origin', 'cookie']; // These are headers we do not forward
207 for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
209 - if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
208 + var cookieStr = '';
209 + for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
210 + if (cookieStr.length > 0) { request += 'cookie: ' + cookieStr + '\r\n' } // If we have session cookies, set them in the header here
211 request += '\r\n';
212
213 if (req.headers['content-length'] != null) {
@@ -244,14 +245,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
245 if (i > 0) { baseurl = req.url.substring(0, i); }
246 if (baseurl.endsWith('/.websocket')) { req.url = baseurl.substring(0, baseurl.length - 11) + ((i < 1) ? '' : req.url.substring(i)); }
247
247 - //console.log('processWebSocket', obj.tunnelId, req.url);
248 -
249 - // Construct the HTTP request and send it out
248 + // Construct the HTTP request
249 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
251 - request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
252 - const blockedHeaders = ['origin', 'host', 'cookie', 'sec-websocket-extensions']; // These are headers we do not forward
250 + const blockedHeaders = ['origin', 'cookie', 'sec-websocket-extensions']; // These are headers we do not forward
251 for (var i in req.headers) { if (blockedHeaders.indexOf(i) == -1) { request += i + ': ' + req.headers[i] + '\r\n'; } }
254 - if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
252 + var cookieStr = '';
253 + for (var i in parent.webCookies) { if (cookieStr != '') { cookieStr += '; ' } cookieStr += (i + '=' + parent.webCookies[i].value); }
254 + if (cookieStr.length > 0) { request += 'cookie: ' + cookieStr + '\r\n' } // If we have session cookies, set them in the header here
255 request += '\r\n';
256 send(Buffer.from(request));
257
@@ -445,7 +445,13 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
445 for (var i in obj.socketHeader) {
446 if (i != 0) {
447 var x2 = obj.socketHeader[i].indexOf(':');
448 - obj.socketXHeader[obj.socketHeader[i].substring(0, x2).toLowerCase()] = obj.socketHeader[i].substring(x2 + 2);
448 + const n = obj.socketHeader[i].substring(0, x2).toLowerCase();
449 + const v = obj.socketHeader[i].substring(x2 + 2);
450 + if (n == 'set-cookie') { // Since "set-cookie" can be present many times in the header, handle it as an array of values
451 + if (obj.socketXHeader[n] == null) { obj.socketXHeader[n] = [v]; } else { obj.socketXHeader[n].push(v); }
452 + } else {
453 + obj.socketXHeader[n] = v;
454 + }
455 }
456 }
457
@@ -568,9 +574,31 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
574 // If there is a header, send it
575 if (header != null) {
576 obj.res.status(parseInt(header.Directive[1])); // Set the status
571 - const blockHeaders = ['Directive']; // These are headers we do not forward
577 + const blockHeaders = ['Directive', 'sec-websocket-extensions']; // We do not forward these headers
578 for (var i in header) {
573 - if (i == 'set-cookie') { parent.webCookie = header[i]; } // Keep the cookie, don't forward it
579 + if (i == 'set-cookie') {
580 + for (var ii in header[i]) {
581 + // Decode the new cookie
582 + //console.log('set-cookie', header[i][ii]);
583 + const cookieSplit = header[i][ii].split(';');
584 + var newCookieName = null, newCookie = {};
585 + for (var j in cookieSplit) {
586 + var l = cookieSplit[j].indexOf('='), k = null, v = null;
587 + if (l == -1) { k = cookieSplit[j].trim(); } else { k = cookieSplit[j].substring(0, l).trim(); v = cookieSplit[j].substring(l + 1).trim(); }
588 + if (j == 0) { newCookieName = k; newCookie.value = v; } else { newCookie[k.toLowerCase()] = (v == null) ? true : v; }
589 + }
590 + if (newCookieName != null) {
591 + if ((typeof newCookie['max-age'] == 'string') && (parseInt(newCookie['max-age']) <= 0)) {
592 + delete parent.webCookies[newCookieName]; // Remove a expired cookie
593 + //console.log('clear-cookie', newCookieName);
594 + } else if (((newCookie.secure != true) || (obj.tls != null))) {
595 + parent.webCookies[newCookieName] = newCookie; // Keep this cookie in the session
596 + if (newCookie.httponly != true) { obj.res.set(i, header[i]); } // if the cookie is not HTTP-only, forward it to the browser. We need to do this to allow JavaScript to read it.
597 + //console.log('new-cookie', newCookieName, newCookie);
598 + }
599 + }
600 + }
601 + }
602 else if (blockHeaders.indexOf(i) == -1) { obj.res.set(i, header[i]); } // Set the headers if not blocked
603 }
604 obj.res.set('Content-Security-Policy', "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;"); // Set an "allow all" policy, see if the can restrict this in the future