Added websocket device to browser frame forwarding, #4172

Ylian Saint-Hilaire committed Jun 28, 2022 at 11:05 UTC 8a8d9869dd3ba9a27e906fa456175f5831ce8557
1 file changed +92 -16
apprelays.js
+92 -16
@@ -229,21 +229,29 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
229
230 // Process a websocket request
231 obj.processWebSocket = function (req, ws) {
232 - //console.log('processWebSocket', req.url);
233 - if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
234 - parent.lastOperation = obj.lastOperation = Date.now();
232 + console.log('processWebSocket', req.url);
233
234 // Mark this tunnel as being a web socket tunnel
237 - obj.isWebSocket = true;
235 + obj.isWebSocket = true;
236 obj.ws = ws;
237
238 + // Pause the websocket until we get a tunnel connected
239 + obj.ws._socket.pause();
240 +
241 + // Remove the trailing '/.websocket' if needed
242 + if (req.url.endsWith('/.websocket')) { req.url = req.url.substring(0, req.url.length - 11); }
243 +
244 + if (obj.relayActive == false) { console.log("ERROR: Attempt to use an unconnected tunnel"); return false; }
245 + parent.lastOperation = obj.lastOperation = Date.now();
246 +
247 // Construct the HTTP request and send it out
248 var request = req.method + ' ' + req.url + ' HTTP/' + req.httpVersion + '\r\n';
249 request += 'host: ' + obj.addr + ':' + obj.port + '\r\n';
243 - const blockedHeaders = ['origin', 'host', 'cookie']; // These are headers we do not forward
250 + const blockedHeaders = ['origin', 'host', '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'; } }
252 if (parent.webCookie != null) { request += 'cookie: ' + parent.webCookie + '\r\n' } // If we have a sessin cookie, use it.
253 request += '\r\n';
254 +
255 send(Buffer.from(request));
256 }
257
@@ -397,7 +405,18 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
405 // Check if this HTTP request has a body
406 if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'close')) { obj.socketParseState = 1; }
407 if (obj.socketXHeader['content-length'] != null) { obj.socketParseState = 1; }
400 - if ((obj.socketXHeader["transfer-encoding"] != null) && (obj.socketXHeader["transfer-encoding"].toLowerCase() == 'chunked')) { obj.socketParseState = 1; }
408 + if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) { obj.socketParseState = 1; }
409 + if (obj.isWebSocket) {
410 + console.log('websocket', obj.socketXHeader);
411 + if ((obj.socketXHeader['connection'] != null) && (obj.socketXHeader['connection'].toLowerCase() == 'upgrade')) {
412 + console.log('websocket pass-thru');
413 + obj.socketParseState = 2; // Switch to decoding websocket frames
414 + obj.ws._socket.resume(); // Resume the browser's websocket
415 + } else {
416 + console.log('websocket failed');
417 + obj.close(); // Failed to upgrade to websocket
418 + }
419 + }
420
421 // Forward the HTTP request into the tunnel, if no body is present, close the request.
422 processHttpResponse(obj.socketXHeader, null, (obj.socketParseState == 0));
@@ -417,7 +436,8 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
436 processHttpResponse(null, data, (obj.socketContentLengthRemaining == 0)); // Send any data we have, if we are done, signal the end of the response
437 if (obj.socketContentLengthRemaining > 0) return; // If more data is needed, return now so we exit the while() loop.
438 csize = 0; // We are done
420 - } else if ((obj.socketXHeader["transfer-encoding"] != null) && (obj.socketXHeader["transfer-encoding"].toLowerCase() == 'chunked')) {
439 + }
440 + else if ((obj.socketXHeader['transfer-encoding'] != null) && (obj.socketXHeader['transfer-encoding'].toLowerCase() == 'chunked')) {
441 // The body is chunked
442 var clen = obj.socketAccumulator.indexOf('\r\n');
443 if (clen < 0) { return; } // Chunk length not found, exit now and get more data.
@@ -438,22 +458,78 @@ module.exports.CreateWebRelay = function (parent, db, args, domain) {
458 if (obj.socketParseState == 2) {
459 // We are in websocket pass-thru mode, decode the websocket frame
460 if (obj.socketAccumulator.length < 2) return; // Need at least 2 bytes to decode a websocket header
441 - console.log('WebSocket frame', obj.socketAccumulator.length, Buffer.from(obj.socketAccumulator, 'binary'));
461 + //console.log('WebSocket frame', obj.socketAccumulator.length, Buffer.from(obj.socketAccumulator, 'binary'));
462
463 + // Decode the websocket frame
464 const buf = Buffer.from(obj.socketAccumulator, 'binary');
465 const fin = ((buf[0] & 0x80) != 0);
466 + const rsv = ((buf[0] & 0x70) != 0);
467 const op = buf[0] & 0x0F;
468 const mask = ((buf[1] & 0x80) != 0);
447 - const len = buf[1] & 0x7F;
448 - console.log('fin', fin);
449 - console.log('op', op);
450 - console.log('mask', mask);
451 - console.log('len', len);
469 + var len = buf[1] & 0x7F;
470 +
471 + // Calculate the total length
472 + var payload = null;
473 + if (len < 126) {
474 + // 1 byte length
475 + if (buf.length < (2 + len)) return; // Insuffisent data
476 + payload = buf.slice(2, 2 + len);
477 + obj.socketAccumulator = obj.socketAccumulator.substring(2 + len); // Remove data from accumulator
478 + } else if (len == 126) {
479 + // 2 byte length
480 + if (buf.length < 4) return;
481 + len = buf.readInt16BE(2);
482 + if (buf.length < (4 + len)) return; // Insuffisent data
483 + payload = buf.slice(4, 4 + len);
484 + obj.socketAccumulator = obj.socketAccumulator.substring(4 + len); // Remove data from accumulator
485 + } if (len == 127) {
486 + // 8 byte length
487 + if (buf.length < 10) return;
488 + len = buf.readInt32BE(2);
489 + if (len > 0) { obj.close(); return; } // This frame is larger than 4 gigabyte, close the connection.
490 + len = buf.readInt32BE(6);
491 + if (buf.length < (10 + len)) return; // Insuffisent data
492 + payload = buf.slice(10, 10 + len);
493 + obj.socketAccumulator = obj.socketAccumulator.substring(10 + len); // Remove data from accumulator
494 + }
495 + if (buf.length < len) return;
496
453 - // Connection close
454 - if ((fin == true) || (op == 8)) { obj.close(); }
497 + // If the mask or reserved bit are true, we are not decoding this right, close the connection.
498 + if ((mask == true) || (rsv == true)) { obj.close(); return; }
499
456 - return;
500 + // TODO: If FIN is not set, we need to add support for continue frames
501 +
502 + // Perform operation
503 + switch (op) {
504 + case 0: { // Continue frame
505 + //console.log('continue', payload.length);
506 + break;
507 + }
508 + case 1: { // Text frame
509 + //console.log('text', payload.length);
510 + try { obj.ws.send(payload.toString('binary')); } catch (ex) { }
511 + break;
512 + }
513 + case 2: { // Binary frame
514 + //console.log('binary', payload.length);
515 + try { obj.ws.send(payload); } catch (ex) { }
516 + break;
517 + }
518 + case 8: { // Connection close
519 + obj.close();
520 + return;
521 + }
522 + case 9: { // Ping frame
523 + //console.log('ping', payload.length);
524 + // TODO
525 + break;
526 + }
527 + case 10: { // Pong frame
528 + //console.log('pong', payload.length);
529 + // TODO
530 + break;
531 + }
532 + }
533 }
534 }
535 }