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
}