CoolFace
Apppublic

strong-tie/inbound-calls

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
receiver.js707 linesDownload Raw Back to lib
1'use strict';2 3const { Writable } = require('stream');4 5const PerMessageDeflate = require('./permessage-deflate');6const {7  BINARY_TYPES,8  EMPTY_BUFFER,9  kStatusCode,10  kWebSocket11} = require('./constants');12const { concat, toArrayBuffer, unmask } = require('./buffer-util');13const { isValidStatusCode, isValidUTF8 } = require('./validation');14 15const FastBuffer = Buffer[Symbol.species];16 17const GET_INFO = 0;18const GET_PAYLOAD_LENGTH_16 = 1;19const GET_PAYLOAD_LENGTH_64 = 2;20const GET_MASK = 3;21const GET_DATA = 4;22const INFLATING = 5;23const DEFER_EVENT = 6;24 25/**26 * HyBi Receiver implementation.27 *28 * @extends Writable29 */30class Receiver extends Writable {31  /**32   * Creates a Receiver instance.33   *34   * @param {Object} [options] Options object35   * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether36   *     any of the `'message'`, `'ping'`, and `'pong'` events can be emitted37   *     multiple times in the same tick38   * @param {String} [options.binaryType=nodebuffer] The type for binary data39   * @param {Object} [options.extensions] An object containing the negotiated40   *     extensions41   * @param {Boolean} [options.isServer=false] Specifies whether to operate in42   *     client or server mode43   * @param {Number} [options.maxPayload=0] The maximum allowed message length44   * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or45   *     not to skip UTF-8 validation for text and close messages46   */47  constructor(options = {}) {48    super();49 50    this._allowSynchronousEvents =51      options.allowSynchronousEvents !== undefined52        ? options.allowSynchronousEvents53        : true;54    this._binaryType = options.binaryType || BINARY_TYPES[0];55    this._extensions = options.extensions || {};56    this._isServer = !!options.isServer;57    this._maxPayload = options.maxPayload | 0;58    this._skipUTF8Validation = !!options.skipUTF8Validation;59    this[kWebSocket] = undefined;60 61    this._bufferedBytes = 0;62    this._buffers = [];63 64    this._compressed = false;65    this._payloadLength = 0;66    this._mask = undefined;67    this._fragmented = 0;68    this._masked = false;69    this._fin = false;70    this._opcode = 0;71 72    this._totalPayloadLength = 0;73    this._messageLength = 0;74    this._fragments = [];75 76    this._errored = false;77    this._loop = false;78    this._state = GET_INFO;79  }80 81  /**82   * Implements `Writable.prototype._write()`.83   *84   * @param {Buffer} chunk The chunk of data to write85   * @param {String} encoding The character encoding of `chunk`86   * @param {Function} cb Callback87   * @private88   */89  _write(chunk, encoding, cb) {90    if (this._opcode === 0x08 && this._state == GET_INFO) return cb();91 92    this._bufferedBytes += chunk.length;93    this._buffers.push(chunk);94    this.startLoop(cb);95  }96 97  /**98   * Consumes `n` bytes from the buffered data.99   *100   * @param {Number} n The number of bytes to consume101   * @return {Buffer} The consumed bytes102   * @private103   */104  consume(n) {105    this._bufferedBytes -= n;106 107    if (n === this._buffers[0].length) return this._buffers.shift();108 109    if (n < this._buffers[0].length) {110      const buf = this._buffers[0];111      this._buffers[0] = new FastBuffer(112        buf.buffer,113        buf.byteOffset + n,114        buf.length - n115      );116 117      return new FastBuffer(buf.buffer, buf.byteOffset, n);118    }119 120    const dst = Buffer.allocUnsafe(n);121 122    do {123      const buf = this._buffers[0];124      const offset = dst.length - n;125 126      if (n >= buf.length) {127        dst.set(this._buffers.shift(), offset);128      } else {129        dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);130        this._buffers[0] = new FastBuffer(131          buf.buffer,132          buf.byteOffset + n,133          buf.length - n134        );135      }136 137      n -= buf.length;138    } while (n > 0);139 140    return dst;141  }142 143  /**144   * Starts the parsing loop.145   *146   * @param {Function} cb Callback147   * @private148   */149  startLoop(cb) {150    this._loop = true;151 152    do {153      switch (this._state) {154        case GET_INFO:155          this.getInfo(cb);156          break;157        case GET_PAYLOAD_LENGTH_16:158          this.getPayloadLength16(cb);159          break;160        case GET_PAYLOAD_LENGTH_64:161          this.getPayloadLength64(cb);162          break;163        case GET_MASK:164          this.getMask();165          break;166        case GET_DATA:167          this.getData(cb);168          break;169        case INFLATING:170        case DEFER_EVENT:171          this._loop = false;172          return;173      }174    } while (this._loop);175 176    if (!this._errored) cb();177  }178 179  /**180   * Reads the first two bytes of a frame.181   *182   * @param {Function} cb Callback183   * @private184   */185  getInfo(cb) {186    if (this._bufferedBytes < 2) {187      this._loop = false;188      return;189    }190 191    const buf = this.consume(2);192 193    if ((buf[0] & 0x30) !== 0x00) {194      const error = this.createError(195        RangeError,196        'RSV2 and RSV3 must be clear',197        true,198        1002,199        'WS_ERR_UNEXPECTED_RSV_2_3'200      );201 202      cb(error);203      return;204    }205 206    const compressed = (buf[0] & 0x40) === 0x40;207 208    if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {209      const error = this.createError(210        RangeError,211        'RSV1 must be clear',212        true,213        1002,214        'WS_ERR_UNEXPECTED_RSV_1'215      );216 217      cb(error);218      return;219    }220 221    this._fin = (buf[0] & 0x80) === 0x80;222    this._opcode = buf[0] & 0x0f;223    this._payloadLength = buf[1] & 0x7f;224 225    if (this._opcode === 0x00) {226      if (compressed) {227        const error = this.createError(228          RangeError,229          'RSV1 must be clear',230          true,231          1002,232          'WS_ERR_UNEXPECTED_RSV_1'233        );234 235        cb(error);236        return;237      }238 239      if (!this._fragmented) {240        const error = this.createError(241          RangeError,242          'invalid opcode 0',243          true,244          1002,245          'WS_ERR_INVALID_OPCODE'246        );247 248        cb(error);249        return;250      }251 252      this._opcode = this._fragmented;253    } else if (this._opcode === 0x01 || this._opcode === 0x02) {254      if (this._fragmented) {255        const error = this.createError(256          RangeError,257          `invalid opcode ${this._opcode}`,258          true,259          1002,260          'WS_ERR_INVALID_OPCODE'261        );262 263        cb(error);264        return;265      }266 267      this._compressed = compressed;268    } else if (this._opcode > 0x07 && this._opcode < 0x0b) {269      if (!this._fin) {270        const error = this.createError(271          RangeError,272          'FIN must be set',273          true,274          1002,275          'WS_ERR_EXPECTED_FIN'276        );277 278        cb(error);279        return;280      }281 282      if (compressed) {283        const error = this.createError(284          RangeError,285          'RSV1 must be clear',286          true,287          1002,288          'WS_ERR_UNEXPECTED_RSV_1'289        );290 291        cb(error);292        return;293      }294 295      if (296        this._payloadLength > 0x7d ||297        (this._opcode === 0x08 && this._payloadLength === 1)298      ) {299        const error = this.createError(300          RangeError,301          `invalid payload length ${this._payloadLength}`,302          true,303          1002,304          'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'305        );306 307        cb(error);308        return;309      }310    } else {311      const error = this.createError(312        RangeError,313        `invalid opcode ${this._opcode}`,314        true,315        1002,316        'WS_ERR_INVALID_OPCODE'317      );318 319      cb(error);320      return;321    }322 323    if (!this._fin && !this._fragmented) this._fragmented = this._opcode;324    this._masked = (buf[1] & 0x80) === 0x80;325 326    if (this._isServer) {327      if (!this._masked) {328        const error = this.createError(329          RangeError,330          'MASK must be set',331          true,332          1002,333          'WS_ERR_EXPECTED_MASK'334        );335 336        cb(error);337        return;338      }339    } else if (this._masked) {340      const error = this.createError(341        RangeError,342        'MASK must be clear',343        true,344        1002,345        'WS_ERR_UNEXPECTED_MASK'346      );347 348      cb(error);349      return;350    }351 352    if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;353    else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;354    else this.haveLength(cb);355  }356 357  /**358   * Gets extended payload length (7+16).359   *360   * @param {Function} cb Callback361   * @private362   */363  getPayloadLength16(cb) {364    if (this._bufferedBytes < 2) {365      this._loop = false;366      return;367    }368 369    this._payloadLength = this.consume(2).readUInt16BE(0);370    this.haveLength(cb);371  }372 373  /**374   * Gets extended payload length (7+64).375   *376   * @param {Function} cb Callback377   * @private378   */379  getPayloadLength64(cb) {380    if (this._bufferedBytes < 8) {381      this._loop = false;382      return;383    }384 385    const buf = this.consume(8);386    const num = buf.readUInt32BE(0);387 388    //389    // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned390    // if payload length is greater than this number.391    //392    if (num > Math.pow(2, 53 - 32) - 1) {393      const error = this.createError(394        RangeError,395        'Unsupported WebSocket frame: payload length > 2^53 - 1',396        false,397        1009,398        'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'399      );400 401      cb(error);402      return;403    }404 405    this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);406    this.haveLength(cb);407  }408 409  /**410   * Payload length has been read.411   *412   * @param {Function} cb Callback413   * @private414   */415  haveLength(cb) {416    if (this._payloadLength && this._opcode < 0x08) {417      this._totalPayloadLength += this._payloadLength;418      if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {419        const error = this.createError(420          RangeError,421          'Max payload size exceeded',422          false,423          1009,424          'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'425        );426 427        cb(error);428        return;429      }430    }431 432    if (this._masked) this._state = GET_MASK;433    else this._state = GET_DATA;434  }435 436  /**437   * Reads mask bytes.438   *439   * @private440   */441  getMask() {442    if (this._bufferedBytes < 4) {443      this._loop = false;444      return;445    }446 447    this._mask = this.consume(4);448    this._state = GET_DATA;449  }450 451  /**452   * Reads data bytes.453   *454   * @param {Function} cb Callback455   * @private456   */457  getData(cb) {458    let data = EMPTY_BUFFER;459 460    if (this._payloadLength) {461      if (this._bufferedBytes < this._payloadLength) {462        this._loop = false;463        return;464      }465 466      data = this.consume(this._payloadLength);467 468      if (469        this._masked &&470        (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0471      ) {472        unmask(data, this._mask);473      }474    }475 476    if (this._opcode > 0x07) {477      this.controlMessage(data, cb);478      return;479    }480 481    if (this._compressed) {482      this._state = INFLATING;483      this.decompress(data, cb);484      return;485    }486 487    if (data.length) {488      //489      // This message is not compressed so its length is the sum of the payload490      // length of all fragments.491      //492      this._messageLength = this._totalPayloadLength;493      this._fragments.push(data);494    }495 496    this.dataMessage(cb);497  }498 499  /**500   * Decompresses data.501   *502   * @param {Buffer} data Compressed data503   * @param {Function} cb Callback504   * @private505   */506  decompress(data, cb) {507    const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];508 509    perMessageDeflate.decompress(data, this._fin, (err, buf) => {510      if (err) return cb(err);511 512      if (buf.length) {513        this._messageLength += buf.length;514        if (this._messageLength > this._maxPayload && this._maxPayload > 0) {515          const error = this.createError(516            RangeError,517            'Max payload size exceeded',518            false,519            1009,520            'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'521          );522 523          cb(error);524          return;525        }526 527        this._fragments.push(buf);528      }529 530      this.dataMessage(cb);531      if (this._state === GET_INFO) this.startLoop(cb);532    });533  }534 535  /**536   * Handles a data message.537   *538   * @param {Function} cb Callback539   * @private540   */541  dataMessage(cb) {542    if (!this._fin) {543      this._state = GET_INFO;544      return;545    }546 547    const messageLength = this._messageLength;548    const fragments = this._fragments;549 550    this._totalPayloadLength = 0;551    this._messageLength = 0;552    this._fragmented = 0;553    this._fragments = [];554 555    if (this._opcode === 2) {556      let data;557 558      if (this._binaryType === 'nodebuffer') {559        data = concat(fragments, messageLength);560      } else if (this._binaryType === 'arraybuffer') {561        data = toArrayBuffer(concat(fragments, messageLength));562      } else if (this._binaryType === 'blob') {563        data = new Blob(fragments);564      } else {565        data = fragments;566      }567 568      if (this._allowSynchronousEvents) {569        this.emit('message', data, true);570        this._state = GET_INFO;571      } else {572        this._state = DEFER_EVENT;573        setImmediate(() => {574          this.emit('message', data, true);575          this._state = GET_INFO;576          this.startLoop(cb);577        });578      }579    } else {580      const buf = concat(fragments, messageLength);581 582      if (!this._skipUTF8Validation && !isValidUTF8(buf)) {583        const error = this.createError(584          Error,585          'invalid UTF-8 sequence',586          true,587          1007,588          'WS_ERR_INVALID_UTF8'589        );590 591        cb(error);592        return;593      }594 595      if (this._state === INFLATING || this._allowSynchronousEvents) {596        this.emit('message', buf, false);597        this._state = GET_INFO;598      } else {599        this._state = DEFER_EVENT;600        setImmediate(() => {601          this.emit('message', buf, false);602          this._state = GET_INFO;603          this.startLoop(cb);604        });605      }606    }607  }608 609  /**610   * Handles a control message.611   *612   * @param {Buffer} data Data to handle613   * @return {(Error|RangeError|undefined)} A possible error614   * @private615   */616  controlMessage(data, cb) {617    if (this._opcode === 0x08) {618      if (data.length === 0) {619        this._loop = false;620        this.emit('conclude', 1005, EMPTY_BUFFER);621        this.end();622      } else {623        const code = data.readUInt16BE(0);624 625        if (!isValidStatusCode(code)) {626          const error = this.createError(627            RangeError,628            `invalid status code ${code}`,629            true,630            1002,631            'WS_ERR_INVALID_CLOSE_CODE'632          );633 634          cb(error);635          return;636        }637 638        const buf = new FastBuffer(639          data.buffer,640          data.byteOffset + 2,641          data.length - 2642        );643 644        if (!this._skipUTF8Validation && !isValidUTF8(buf)) {645          const error = this.createError(646            Error,647            'invalid UTF-8 sequence',648            true,649            1007,650            'WS_ERR_INVALID_UTF8'651          );652 653          cb(error);654          return;655        }656 657        this._loop = false;658        this.emit('conclude', code, buf);659        this.end();660      }661 662      this._state = GET_INFO;663      return;664    }665 666    if (this._allowSynchronousEvents) {667      this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);668      this._state = GET_INFO;669    } else {670      this._state = DEFER_EVENT;671      setImmediate(() => {672        this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);673        this._state = GET_INFO;674        this.startLoop(cb);675      });676    }677  }678 679  /**680   * Builds an error object.681   *682   * @param {function(new:Error|RangeError)} ErrorCtor The error constructor683   * @param {String} message The error message684   * @param {Boolean} prefix Specifies whether or not to add a default prefix to685   *     `message`686   * @param {Number} statusCode The status code687   * @param {String} errorCode The exposed error code688   * @return {(Error|RangeError)} The error689   * @private690   */691  createError(ErrorCtor, message, prefix, statusCode, errorCode) {692    this._loop = false;693    this._errored = true;694 695    const err = new ErrorCtor(696      prefix ? `Invalid WebSocket frame: ${message}` : message697    );698 699    Error.captureStackTrace(err, this.createError);700    err.code = errorCode;701    err[kStatusCode] = statusCode;702    return err;703  }704}705 706module.exports = Receiver;707