'use strict'; const { Writable } = require('stream'); const PerMessageDeflate = require('./permessage-deflate'); const { BINARY_TYPES, EMPTY_BUFFER, kStatusCode, kWebSocket } = require('./constants'); const { concat, toArrayBuffer, unmask } = require('./buffer-util'); const { isValidStatusCode, isValidUTF8 } = require('./validation'); const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; const GET_MASK = 3; const GET_DATA = 4; const INFLATING = 5; /** * HyBi Receiver implementation. * * @extends Writable */ class Receiver extends Writable { /** * Creates a Receiver instance. * * @param {String} [binaryType=nodebuffer] The type for binary data * @param {Object} [extensions] An object containing the negotiated extensions * @param {Boolean} [isServer=false] Specifies whether to operate in client or * server mode * @param {Number} [maxPayload=0] The maximum allowed message length */ constructor(binaryType, extensions, isServer, maxPayload) { super(); this._binaryType = binaryType || BINARY_TYPES[0]; this[kWebSocket] = undefined; this._extensions = extensions || {}; this._isServer = !!isServer; this._maxPayload = maxPayload | 0; this._bufferedBytes = 0; this._buffers = []; this._compressed = false; this._payloadLength = 0; this._mask = undefined; this._fragmented = 0; this._masked = false; this._fin = false; this._opcode = 0; this._totalPayloadLength = 0; this._messageLength = 0; this._fragments = []; this._state = GET_INFO; this._loop = false; } /** * Implements `Writable.prototype._write()`. * * @param {Buffer} chunk The chunk of data to write * @param {String} encoding The character encoding of `chunk` * @param {Function} cb Callback * @private */ _write(chunk, encoding, cb) { if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); this._bufferedBytes += chunk.length; this._buffers.push(chunk); this.startLoop(cb); } /** * Consumes `n` bytes from the buffered data. * * @param {Number} n The number of bytes to consume * @return {Buffer} The consumed bytes * @private */ consume(n) { this._bufferedBytes -= n; if (n === this._buffers[0].length) return this._buffers.shift(); if (n < this._buffers[0].length) { const buf = this._buffers[0]; this._buffers[0] = buf.slice(n); return buf.slice(0, n); } const dst = Buffer.allocUnsafe(n); do { const buf = this._buffers[0]; const offset = dst.length - n; if (n >= buf.length) { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); this._buffers[0] = buf.slice(n); } n -= buf.length; } while (n > 0); return dst; } /** * Starts the parsing loop. * * @param {Function} cb Callback * @private */ startLoop(cb) { let err; this._loop = true; do { switch (this._state) { case GET_INFO: err = this.getInfo(); break; case GET_PAYLOAD_LENGTH_16: err = this.getPayloadLength16(); break; case GET_PAYLOAD_LENGTH_64: err = this.getPayloadLength64(); break; case GET_MASK: this.getMask(); break; case GET_DATA: err = this.getData(cb); break; default: // `INFLATING` this._loop = false; return; } } while (this._loop); cb(err); } /** * Reads the first two bytes of a frame. * * @return {(RangeError|undefined)} A possible error * @private */ getInfo() { if (this._bufferedBytes < 2) { this._loop = false; return; } const buf = this.consume(2); if ((buf[0] & 0x30) !== 0x00) { this._loop = false; return error( RangeError, 'RSV2 and RSV3 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_2_3' ); } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { this._loop = false; return error( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); } this._fin = (buf[0] & 0x80) === 0x80; this._opcode = buf[0] & 0x0f; this._payloadLength = buf[1] & 0x7f; if (this._opcode === 0x00) { if (compressed) { this._loop = false; return error( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); } if (!this._fragmented) { this._loop = false; return error( RangeError, 'invalid opcode 0', true, 1002, 'WS_ERR_INVALID_OPCODE' ); } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { this._loop = false; return error( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { this._loop = false; return error( RangeError, 'FIN must be set', true, 1002, 'WS_ERR_EXPECTED_FIN' ); } if (compressed) { this._loop = false; return error( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); } if (this._payloadLength > 0x7d) { this._loop = false; return error( RangeError, `invalid payload length ${this._payloadLength}`, true, 1002, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } } else { this._loop = false; return error( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; this._masked = (buf[1] & 0x80) === 0x80; if (this._isServer) { if (!this._masked) { this._loop = false; return error( RangeError, 'MASK must be set', true, 1002, 'WS_ERR_EXPECTED_MASK' ); } } else if (this._masked) { this._loop = false; return error( RangeError, 'MASK must be clear', true, 1002, 'WS_ERR_UNEXPECTED_MASK' ); } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; else return this.haveLength(); } /** * Gets extended payload length (7+16). * * @return {(RangeError|undefined)} A possible error * @private */ getPayloadLength16() { if (this._bufferedBytes < 2) { this._loop = false; return; } this._payloadLength = this.consume(2).readUInt16BE(0); return this.haveLength(); } /** * Gets extended payload length (7+64). * * @return {(RangeError|undefined)} A possible error * @private */ getPayloadLength64() { if (this._bufferedBytes < 8) { this._loop = false; return; } const buf = this.consume(8); const num = buf.readUInt32BE(0); // // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned // if payload length is greater than this number. // if (num > Math.pow(2, 53 - 32) - 1) { this._loop = false; return error( RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, 1009, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); } this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); return this.haveLength(); } /** * Payload length has been read. * * @return {(RangeError|undefined)} A possible error * @private */ haveLength() { if (this._payloadLength && this._opcode < 0x08) { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { this._loop = false; return error( RangeError, 'Max payload size exceeded', false, 1009, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ); } } if (this._masked) this._state = GET_MASK; else this._state = GET_DATA; } /** * Reads mask bytes. * * @private */ getMask() { if (this._bufferedBytes < 4) { this._loop = false; return; } this._mask = this.consume(4); this._state = GET_DATA; } /** * Reads data bytes. * * @param {Function} cb Callback * @return {(Error|RangeError|undefined)} A possible error * @private */ getData(cb) { let data = EMPTY_BUFFER; if (this._payloadLength) { if (this._bufferedBytes < this._payloadLength) { this._loop = false; return; } data = this.consume(this._payloadLength); if (this._masked) unmask(data, this._mask); } if (this._opcode > 0x07) return this.controlMessage(data); if (this._compressed) { this._state = INFLATING; this.decompress(data, cb); return; } if (data.length) { // // This message is not compressed so its lenght is the sum of the payload // length of all fragments. // this._messageLength = this._totalPayloadLength; this._fragments.push(data); } return this.dataMessage(); } /** * Decompresses data. * * @param {Buffer} data Compressed data * @param {Function} cb Callback * @private */ decompress(data, cb) { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; perMessageDeflate.decompress(data, this._fin, (err, buf) => { if (err) return cb(err); if (buf.length) { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { return cb( error( RangeError, 'Max payload size exceeded', false, 1009, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ) ); } this._fragments.push(buf); } const er = this.dataMessage(); if (er) return cb(er); this.startLoop(cb); }); } /** * Handles a data message. * * @return {(Error|undefined)} A possible error * @private */ dataMessage() { if (this._fin) { const messageLength = this._messageLength; const fragments = this._fragments; this._totalPayloadLength = 0; this._messageLength = 0; this._fragmented = 0; this._fragments = []; if (this._opcode === 2) { let data; if (this._binaryType === 'nodebuffer') { data = concat(fragments, messageLength); } else if (this._binaryType === 'arraybuffer') { data = toArrayBuffer(concat(fragments, messageLength)); } else { data = fragments; } this.emit('message', data); } else { const buf = concat(fragments, messageLength); if (!isValidUTF8(buf)) { this._loop = false; return error( Error, 'invalid UTF-8 sequence', true, 1007, 'WS_ERR_INVALID_UTF8' ); } this.emit('message', buf.toString()); } } this._state = GET_INFO; } /** * Handles a control message. * * @param {Buffer} data Data to handle * @return {(Error|RangeError|undefined)} A possible error * @private */ controlMessage(data) { if (this._opcode === 0x08) { this._loop = false; if (data.length === 0) { this.emit('conclude', 1005, ''); this.end(); } else if (data.length === 1) { return error( RangeError, 'invalid payload length 1', true, 1002, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { return error( RangeError, `invalid status code ${code}`, true, 1002, 'WS_ERR_INVALID_CLOSE_CODE' ); } const buf = data.slice(2); if (!isValidUTF8(buf)) { return error( Error, 'invalid UTF-8 sequence', true, 1007, 'WS_ERR_INVALID_UTF8' ); } this.emit('conclude', code, buf.toString()); this.end(); } } else if (this._opcode === 0x09) { this.emit('ping', data); } else { this.emit('pong', data); } this._state = GET_INFO; } } module.exports = Receiver; /** * Builds an error object. * * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ function error(ErrorCtor, message, prefix, statusCode, errorCode) { const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, error); err.code = errorCode; err[kStatusCode] = statusCode; return err; }