JavaScript 65.5%
Python 17.8%
CSS 13%
HTML 3.7%
1'use strict';23const { Writable } = require('stream');45const 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');1415const FastBuffer = Buffer[Symbol.species];1617const 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;2425/**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.maxBufferedChunks=0] The maximum number of44 * buffered data chunks45 * @param {Number} [options.maxFragments=0] The maximum number of message46 * fragments47 * @param {Number} [options.maxPayload=0] The maximum allowed message length48 * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or49 * not to skip UTF-8 validation for text and close messages50 */51 constructor(options = {}) {52 super();5354 this._allowSynchronousEvents =55 options.allowSynchronousEvents !== undefined56 ? options.allowSynchronousEvents57 : true;58 this._binaryType = options.binaryType || BINARY_TYPES[0];59 this._extensions = options.extensions || {};60 this._isServer = !!options.isServer;61 this._maxBufferedChunks = options.maxBufferedChunks | 0;62 this._maxFragments = options.maxFragments | 0;63 this._maxPayload = options.maxPayload | 0;64 this._skipUTF8Validation = !!options.skipUTF8Validation;65 this[kWebSocket] = undefined;6667 this._bufferedBytes = 0;68 this._buffers = [];6970 this._compressed = false;71 this._payloadLength = 0;72 this._mask = undefined;73 this._fragmented = 0;74 this._masked = false;75 this._fin = false;76 this._opcode = 0;7778 this._totalPayloadLength = 0;79 this._messageLength = 0;80 this._numFragments = 0;81 this._fragments = [];8283 this._errored = false;84 this._loop = false;85 this._state = GET_INFO;86 }8788 /**89 * Implements `Writable.prototype._write()`.90 *91 * @param {Buffer} chunk The chunk of data to write92 * @param {String} encoding The character encoding of `chunk`93 * @param {Function} cb Callback94 * @private95 */96 _write(chunk, encoding, cb) {97 if (this._opcode === 0x08 && this._state == GET_INFO) return cb();9899 if (100 this._maxBufferedChunks > 0 &&101 this._buffers.length >= this._maxBufferedChunks102 ) {103 cb(104 this.createError(105 RangeError,106 'Too many buffered chunks',107 false,108 1008,109 'WS_ERR_TOO_MANY_BUFFERED_PARTS'110 )111 );112 return;113 }114115 this._bufferedBytes += chunk.length;116 this._buffers.push(chunk);117 this.startLoop(cb);118 }119120 /**121 * Consumes `n` bytes from the buffered data.122 *123 * @param {Number} n The number of bytes to consume124 * @return {Buffer} The consumed bytes125 * @private126 */127 consume(n) {128 this._bufferedBytes -= n;129130 if (n === this._buffers[0].length) return this._buffers.shift();131132 if (n < this._buffers[0].length) {133 const buf = this._buffers[0];134 this._buffers[0] = new FastBuffer(135 buf.buffer,136 buf.byteOffset + n,137 buf.length - n138 );139140 return new FastBuffer(buf.buffer, buf.byteOffset, n);141 }142143 const dst = Buffer.allocUnsafe(n);144145 do {146 const buf = this._buffers[0];147 const offset = dst.length - n;148149 if (n >= buf.length) {150 dst.set(this._buffers.shift(), offset);151 } else {152 dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);153 this._buffers[0] = new FastBuffer(154 buf.buffer,155 buf.byteOffset + n,156 buf.length - n157 );158 }159160 n -= buf.length;161 } while (n > 0);162163 return dst;164 }165166 /**167 * Starts the parsing loop.168 *169 * @param {Function} cb Callback170 * @private171 */172 startLoop(cb) {173 this._loop = true;174175 do {176 switch (this._state) {177 case GET_INFO:178 this.getInfo(cb);179 break;180 case GET_PAYLOAD_LENGTH_16:181 this.getPayloadLength16(cb);182 break;183 case GET_PAYLOAD_LENGTH_64:184 this.getPayloadLength64(cb);185 break;186 case GET_MASK:187 this.getMask();188 break;189 case GET_DATA:190 this.getData(cb);191 break;192 case INFLATING:193 case DEFER_EVENT:194 this._loop = false;195 return;196 }197 } while (this._loop);198199 if (!this._errored) cb();200 }201202 /**203 * Reads the first two bytes of a frame.204 *205 * @param {Function} cb Callback206 * @private207 */208 getInfo(cb) {209 if (this._bufferedBytes < 2) {210 this._loop = false;211 return;212 }213214 const buf = this.consume(2);215216 if ((buf[0] & 0x30) !== 0x00) {217 const error = this.createError(218 RangeError,219 'RSV2 and RSV3 must be clear',220 true,221 1002,222 'WS_ERR_UNEXPECTED_RSV_2_3'223 );224225 cb(error);226 return;227 }228229 const compressed = (buf[0] & 0x40) === 0x40;230231 if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {232 const error = this.createError(233 RangeError,234 'RSV1 must be clear',235 true,236 1002,237 'WS_ERR_UNEXPECTED_RSV_1'238 );239240 cb(error);241 return;242 }243244 this._fin = (buf[0] & 0x80) === 0x80;245 this._opcode = buf[0] & 0x0f;246 this._payloadLength = buf[1] & 0x7f;247248 if (this._opcode === 0x00) {249 if (compressed) {250 const error = this.createError(251 RangeError,252 'RSV1 must be clear',253 true,254 1002,255 'WS_ERR_UNEXPECTED_RSV_1'256 );257258 cb(error);259 return;260 }261262 if (!this._fragmented) {263 const error = this.createError(264 RangeError,265 'invalid opcode 0',266 true,267 1002,268 'WS_ERR_INVALID_OPCODE'269 );270271 cb(error);272 return;273 }274275 this._opcode = this._fragmented;276 } else if (this._opcode === 0x01 || this._opcode === 0x02) {277 if (this._fragmented) {278 const error = this.createError(279 RangeError,280 `invalid opcode ${this._opcode}`,281 true,282 1002,283 'WS_ERR_INVALID_OPCODE'284 );285286 cb(error);287 return;288 }289290 this._compressed = compressed;291 } else if (this._opcode > 0x07 && this._opcode < 0x0b) {292 if (!this._fin) {293 const error = this.createError(294 RangeError,295 'FIN must be set',296 true,297 1002,298 'WS_ERR_EXPECTED_FIN'299 );300301 cb(error);302 return;303 }304305 if (compressed) {306 const error = this.createError(307 RangeError,308 'RSV1 must be clear',309 true,310 1002,311 'WS_ERR_UNEXPECTED_RSV_1'312 );313314 cb(error);315 return;316 }317318 if (319 this._payloadLength > 0x7d ||320 (this._opcode === 0x08 && this._payloadLength === 1)321 ) {322 const error = this.createError(323 RangeError,324 `invalid payload length ${this._payloadLength}`,325 true,326 1002,327 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'328 );329330 cb(error);331 return;332 }333 } else {334 const error = this.createError(335 RangeError,336 `invalid opcode ${this._opcode}`,337 true,338 1002,339 'WS_ERR_INVALID_OPCODE'340 );341342 cb(error);343 return;344 }345346 if (!this._fin && !this._fragmented) this._fragmented = this._opcode;347 this._masked = (buf[1] & 0x80) === 0x80;348349 if (this._isServer) {350 if (!this._masked) {351 const error = this.createError(352 RangeError,353 'MASK must be set',354 true,355 1002,356 'WS_ERR_EXPECTED_MASK'357 );358359 cb(error);360 return;361 }362 } else if (this._masked) {363 const error = this.createError(364 RangeError,365 'MASK must be clear',366 true,367 1002,368 'WS_ERR_UNEXPECTED_MASK'369 );370371 cb(error);372 return;373 }374375 if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;376 else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;377 else this.haveLength(cb);378 }379380 /**381 * Gets extended payload length (7+16).382 *383 * @param {Function} cb Callback384 * @private385 */386 getPayloadLength16(cb) {387 if (this._bufferedBytes < 2) {388 this._loop = false;389 return;390 }391392 this._payloadLength = this.consume(2).readUInt16BE(0);393 this.haveLength(cb);394 }395396 /**397 * Gets extended payload length (7+64).398 *399 * @param {Function} cb Callback400 * @private401 */402 getPayloadLength64(cb) {403 if (this._bufferedBytes < 8) {404 this._loop = false;405 return;406 }407408 const buf = this.consume(8);409 const num = buf.readUInt32BE(0);410411 //412 // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned413 // if payload length is greater than this number.414 //415 if (num > Math.pow(2, 53 - 32) - 1) {416 const error = this.createError(417 RangeError,418 'Unsupported WebSocket frame: payload length > 2^53 - 1',419 false,420 1009,421 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'422 );423424 cb(error);425 return;426 }427428 this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);429 this.haveLength(cb);430 }431432 /**433 * Payload length has been read.434 *435 * @param {Function} cb Callback436 * @private437 */438 haveLength(cb) {439 if (this._payloadLength && this._opcode < 0x08) {440 this._totalPayloadLength += this._payloadLength;441 if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {442 const error = this.createError(443 RangeError,444 'Max payload size exceeded',445 false,446 1009,447 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'448 );449450 cb(error);451 return;452 }453 }454455 if (this._masked) this._state = GET_MASK;456 else this._state = GET_DATA;457 }458459 /**460 * Reads mask bytes.461 *462 * @private463 */464 getMask() {465 if (this._bufferedBytes < 4) {466 this._loop = false;467 return;468 }469470 this._mask = this.consume(4);471 this._state = GET_DATA;472 }473474 /**475 * Reads data bytes.476 *477 * @param {Function} cb Callback478 * @private479 */480 getData(cb) {481 let data = EMPTY_BUFFER;482483 if (this._payloadLength) {484 if (this._bufferedBytes < this._payloadLength) {485 this._loop = false;486 return;487 }488489 data = this.consume(this._payloadLength);490491 if (492 this._masked &&493 (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0494 ) {495 unmask(data, this._mask);496 }497 }498499 if (this._opcode > 0x07) {500 this.controlMessage(data, cb);501 return;502 }503504 if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) {505 const error = this.createError(506 RangeError,507 'Too many message fragments',508 false,509 1008,510 'WS_ERR_TOO_MANY_BUFFERED_PARTS'511 );512513 cb(error);514 return;515 }516517 if (this._compressed) {518 this._state = INFLATING;519 this.decompress(data, cb);520 return;521 }522523 if (data.length) {524 //525 // This message is not compressed so its length is the sum of the payload526 // length of all fragments.527 //528 this._messageLength = this._totalPayloadLength;529 this._fragments.push(data);530 }531532 this.dataMessage(cb);533 }534535 /**536 * Decompresses data.537 *538 * @param {Buffer} data Compressed data539 * @param {Function} cb Callback540 * @private541 */542 decompress(data, cb) {543 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];544545 perMessageDeflate.decompress(data, this._fin, (err, buf) => {546 if (err) return cb(err);547548 if (buf.length) {549 this._messageLength += buf.length;550 if (this._messageLength > this._maxPayload && this._maxPayload > 0) {551 const error = this.createError(552 RangeError,553 'Max payload size exceeded',554 false,555 1009,556 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'557 );558559 cb(error);560 return;561 }562563 this._fragments.push(buf);564 }565566 this.dataMessage(cb);567 if (this._state === GET_INFO) this.startLoop(cb);568 });569 }570571 /**572 * Handles a data message.573 *574 * @param {Function} cb Callback575 * @private576 */577 dataMessage(cb) {578 if (!this._fin) {579 this._state = GET_INFO;580 return;581 }582583 const messageLength = this._messageLength;584 const fragments = this._fragments;585586 this._totalPayloadLength = 0;587 this._messageLength = 0;588 this._fragmented = 0;589 this._numFragments = 0;590 this._fragments = [];591592 if (this._opcode === 2) {593 let data;594595 if (this._binaryType === 'nodebuffer') {596 data = concat(fragments, messageLength);597 } else if (this._binaryType === 'arraybuffer') {598 data = toArrayBuffer(concat(fragments, messageLength));599 } else if (this._binaryType === 'blob') {600 data = new Blob(fragments);601 } else {602 data = fragments;603 }604605 if (this._allowSynchronousEvents) {606 this.emit('message', data, true);607 this._state = GET_INFO;608 } else {609 this._state = DEFER_EVENT;610 setImmediate(() => {611 this.emit('message', data, true);612 this._state = GET_INFO;613 this.startLoop(cb);614 });615 }616 } else {617 const buf = concat(fragments, messageLength);618619 if (!this._skipUTF8Validation && !isValidUTF8(buf)) {620 const error = this.createError(621 Error,622 'invalid UTF-8 sequence',623 true,624 1007,625 'WS_ERR_INVALID_UTF8'626 );627628 cb(error);629 return;630 }631632 if (this._state === INFLATING || this._allowSynchronousEvents) {633 this.emit('message', buf, false);634 this._state = GET_INFO;635 } else {636 this._state = DEFER_EVENT;637 setImmediate(() => {638 this.emit('message', buf, false);639 this._state = GET_INFO;640 this.startLoop(cb);641 });642 }643 }644 }645646 /**647 * Handles a control message.648 *649 * @param {Buffer} data Data to handle650 * @return {(Error|RangeError|undefined)} A possible error651 * @private652 */653 controlMessage(data, cb) {654 if (this._opcode === 0x08) {655 if (data.length === 0) {656 this._loop = false;657 this.emit('conclude', 1005, EMPTY_BUFFER);658 this.end();659 } else {660 const code = data.readUInt16BE(0);661662 if (!isValidStatusCode(code)) {663 const error = this.createError(664 RangeError,665 `invalid status code ${code}`,666 true,667 1002,668 'WS_ERR_INVALID_CLOSE_CODE'669 );670671 cb(error);672 return;673 }674675 const buf = new FastBuffer(676 data.buffer,677 data.byteOffset + 2,678 data.length - 2679 );680681 if (!this._skipUTF8Validation && !isValidUTF8(buf)) {682 const error = this.createError(683 Error,684 'invalid UTF-8 sequence',685 true,686 1007,687 'WS_ERR_INVALID_UTF8'688 );689690 cb(error);691 return;692 }693694 this._loop = false;695 this.emit('conclude', code, buf);696 this.end();697 }698699 this._state = GET_INFO;700 return;701 }702703 if (this._allowSynchronousEvents) {704 this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);705 this._state = GET_INFO;706 } else {707 this._state = DEFER_EVENT;708 setImmediate(() => {709 this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);710 this._state = GET_INFO;711 this.startLoop(cb);712 });713 }714 }715716 /**717 * Builds an error object.718 *719 * @param {function(new:Error|RangeError)} ErrorCtor The error constructor720 * @param {String} message The error message721 * @param {Boolean} prefix Specifies whether or not to add a default prefix to722 * `message`723 * @param {Number} statusCode The status code724 * @param {String} errorCode The exposed error code725 * @return {(Error|RangeError)} The error726 * @private727 */728 createError(ErrorCtor, message, prefix, statusCode, errorCode) {729 this._loop = false;730 this._errored = true;731732 const err = new ErrorCtor(733 prefix ? `Invalid WebSocket frame: ${message}` : message734 );735736 Error.captureStackTrace(err, this.createError);737 err.code = errorCode;738 err[kStatusCode] = statusCode;739 return err;740 }741}742743module.exports = Receiver;744