diff --git a/dist/index.js b/dist/index.js index a906969..f4d2304 100644 --- a/dist/index.js +++ b/dist/index.js @@ -62999,10 +62999,16 @@ if (!process.env.WS_NO_BUFFER_UTIL) { "use strict"; +const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments']; +const hasBlob = typeof Blob !== 'undefined'; + +if (hasBlob) BINARY_TYPES.push('blob'); + module.exports = { - BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + BINARY_TYPES, EMPTY_BUFFER: Buffer.alloc(0), GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + hasBlob, kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), kListener: Symbol('kListener'), kStatusCode: Symbol('status-code'), @@ -64128,12 +64134,14 @@ const { concat, toArrayBuffer, unmask } = __nccwpck_require__(9436); const { isValidStatusCode, isValidUTF8 } = __nccwpck_require__(86279); const FastBuffer = Buffer[Symbol.species]; + 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; +const DEFER_EVENT = 6; /** * HyBi Receiver implementation. @@ -64145,6 +64153,9 @@ class Receiver extends Writable { * Creates a Receiver instance. * * @param {Object} [options] Options object + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick * @param {String} [options.binaryType=nodebuffer] The type for binary data * @param {Object} [options.extensions] An object containing the negotiated * extensions @@ -64157,6 +64168,10 @@ class Receiver extends Writable { constructor(options = {}) { super(); + this._allowSynchronousEvents = + options.allowSynchronousEvents !== undefined + ? options.allowSynchronousEvents + : true; this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; @@ -64179,8 +64194,9 @@ class Receiver extends Writable { this._messageLength = 0; this._fragments = []; - this._state = GET_INFO; + this._errored = false; this._loop = false; + this._state = GET_INFO; } /** @@ -64252,43 +64268,42 @@ class Receiver extends Writable { * @private */ startLoop(cb) { - let err; this._loop = true; do { switch (this._state) { case GET_INFO: - err = this.getInfo(); + this.getInfo(cb); break; case GET_PAYLOAD_LENGTH_16: - err = this.getPayloadLength16(); + this.getPayloadLength16(cb); break; case GET_PAYLOAD_LENGTH_64: - err = this.getPayloadLength64(); + this.getPayloadLength64(cb); break; case GET_MASK: this.getMask(); break; case GET_DATA: - err = this.getData(cb); + this.getData(cb); break; - default: - // `INFLATING` + case INFLATING: + case DEFER_EVENT: this._loop = false; return; } } while (this._loop); - cb(err); + if (!this._errored) cb(); } /** * Reads the first two bytes of a frame. * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getInfo() { + getInfo(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; @@ -64297,27 +64312,31 @@ class Receiver extends Writable { const buf = this.consume(2); if ((buf[0] & 0x30) !== 0x00) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV2 and RSV3 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_2_3' ); + + cb(error); + return; } const compressed = (buf[0] & 0x40) === 0x40; if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } this._fin = (buf[0] & 0x80) === 0x80; @@ -64326,86 +64345,100 @@ class Receiver extends Writable { if (this._opcode === 0x00) { if (compressed) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } if (!this._fragmented) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'invalid opcode 0', true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } this._opcode = this._fragmented; } else if (this._opcode === 0x01 || this._opcode === 0x02) { if (this._fragmented) { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } this._compressed = compressed; } else if (this._opcode > 0x07 && this._opcode < 0x0b) { if (!this._fin) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'FIN must be set', true, 1002, 'WS_ERR_EXPECTED_FIN' ); + + cb(error); + return; } if (compressed) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'RSV1 must be clear', true, 1002, 'WS_ERR_UNEXPECTED_RSV_1' ); + + cb(error); + return; } if ( this._payloadLength > 0x7d || (this._opcode === 0x08 && this._payloadLength === 1) ) { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid payload length ${this._payloadLength}`, true, 1002, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' ); + + cb(error); + return; } } else { - this._loop = false; - return error( + const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, 'WS_ERR_INVALID_OPCODE' ); + + cb(error); + return; } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; @@ -64413,54 +64446,58 @@ class Receiver extends Writable { if (this._isServer) { if (!this._masked) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'MASK must be set', true, 1002, 'WS_ERR_EXPECTED_MASK' ); + + cb(error); + return; } } else if (this._masked) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'MASK must be clear', true, 1002, 'WS_ERR_UNEXPECTED_MASK' ); + + cb(error); + return; } 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(); + else this.haveLength(cb); } /** * Gets extended payload length (7+16). * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getPayloadLength16() { + getPayloadLength16(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; } this._payloadLength = this.consume(2).readUInt16BE(0); - return this.haveLength(); + this.haveLength(cb); } /** * Gets extended payload length (7+64). * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - getPayloadLength64() { + getPayloadLength64(cb) { if (this._bufferedBytes < 8) { this._loop = false; return; @@ -64474,38 +64511,42 @@ class Receiver extends Writable { // if payload length is greater than this number. // if (num > Math.pow(2, 53 - 32) - 1) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'Unsupported WebSocket frame: payload length > 2^53 - 1', false, 1009, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' ); + + cb(error); + return; } this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); - return this.haveLength(); + this.haveLength(cb); } /** * Payload length has been read. * - * @return {(RangeError|undefined)} A possible error + * @param {Function} cb Callback * @private */ - haveLength() { + haveLength(cb) { if (this._payloadLength && this._opcode < 0x08) { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { - this._loop = false; - return error( + const error = this.createError( RangeError, 'Max payload size exceeded', false, 1009, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ); + + cb(error); + return; } } @@ -64532,7 +64573,6 @@ class Receiver extends Writable { * Reads data bytes. * * @param {Function} cb Callback - * @return {(Error|RangeError|undefined)} A possible error * @private */ getData(cb) { @@ -64554,7 +64594,10 @@ class Receiver extends Writable { } } - if (this._opcode > 0x07) return this.controlMessage(data); + if (this._opcode > 0x07) { + this.controlMessage(data, cb); + return; + } if (this._compressed) { this._state = INFLATING; @@ -64571,7 +64614,7 @@ class Receiver extends Writable { this._fragments.push(data); } - return this.dataMessage(); + this.dataMessage(cb); } /** @@ -64590,74 +64633,98 @@ class Receiver extends Writable { 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' - ) + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' ); + + cb(error); + return; } this._fragments.push(buf); } - const er = this.dataMessage(); - if (er) return cb(er); - - this.startLoop(cb); + this.dataMessage(cb); + if (this._state === GET_INFO) this.startLoop(cb); }); } /** * Handles a data message. * - * @return {(Error|undefined)} A possible error + * @param {Function} cb Callback * @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; - } + dataMessage(cb) { + if (!this._fin) { + this._state = GET_INFO; + return; + } + + 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 if (this._binaryType === 'blob') { + data = new Blob(fragments); + } else { + data = fragments; + } + + if (this._allowSynchronousEvents) { this.emit('message', data, true); + this._state = GET_INFO; } else { - const buf = concat(fragments, messageLength); + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', data, true); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } else { + const buf = concat(fragments, messageLength); - if (!this._skipUTF8Validation && !isValidUTF8(buf)) { - this._loop = false; - return error( - Error, - 'invalid UTF-8 sequence', - true, - 1007, - 'WS_ERR_INVALID_UTF8' - ); - } + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + cb(error); + return; + } + + if (this._state === INFLATING || this._allowSynchronousEvents) { this.emit('message', buf, false); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', buf, false); + this._state = GET_INFO; + this.startLoop(cb); + }); } } - - this._state = GET_INFO; } /** @@ -64667,24 +64734,26 @@ class Receiver extends Writable { * @return {(Error|RangeError|undefined)} A possible error * @private */ - controlMessage(data) { + controlMessage(data, cb) { if (this._opcode === 0x08) { - this._loop = false; - if (data.length === 0) { + this._loop = false; this.emit('conclude', 1005, EMPTY_BUFFER); this.end(); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { - return error( + const error = this.createError( RangeError, `invalid status code ${code}`, true, 1002, 'WS_ERR_INVALID_CLOSE_CODE' ); + + cb(error); + return; } const buf = new FastBuffer( @@ -64694,53 +64763,69 @@ class Receiver extends Writable { ); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { - return error( + const error = this.createError( Error, 'invalid UTF-8 sequence', true, 1007, 'WS_ERR_INVALID_UTF8' ); + + cb(error); + return; } + this._loop = false; this.emit('conclude', code, buf); this.end(); } - } else if (this._opcode === 0x09) { - this.emit('ping', data); - } else { - this.emit('pong', data); + + this._state = GET_INFO; + return; } - this._state = GET_INFO; + if (this._allowSynchronousEvents) { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + this.startLoop(cb); + }); + } } -} -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 + */ + createError(ErrorCtor, message, prefix, statusCode, errorCode) { + this._loop = false; + this._errored = true; -/** - * 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 - ); + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); - Error.captureStackTrace(err, error); - err.code = errorCode; - err[kStatusCode] = statusCode; - return err; + Error.captureStackTrace(err, this.createError); + err.code = errorCode; + err[kStatusCode] = statusCode; + return err; + } } +module.exports = Receiver; + /***/ }), @@ -64748,21 +64833,27 @@ function error(ErrorCtor, message, prefix, statusCode, errorCode) { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ -const net = __nccwpck_require__(41808); -const tls = __nccwpck_require__(24404); +const { Duplex } = __nccwpck_require__(12781); const { randomFillSync } = __nccwpck_require__(6113); const PerMessageDeflate = __nccwpck_require__(56684); -const { EMPTY_BUFFER } = __nccwpck_require__(15949); -const { isValidStatusCode } = __nccwpck_require__(86279); +const { EMPTY_BUFFER, kWebSocket, NOOP } = __nccwpck_require__(15949); +const { isBlob, isValidStatusCode } = __nccwpck_require__(86279); const { mask: applyMask, toBuffer } = __nccwpck_require__(9436); const kByteLength = Symbol('kByteLength'); const maskBuffer = Buffer.alloc(4); +const RANDOM_POOL_SIZE = 8 * 1024; +let randomPool; +let randomPoolPointer = RANDOM_POOL_SIZE; + +const DEFAULT = 0; +const DEFLATING = 1; +const GET_BLOB_DATA = 2; /** * HyBi Sender implementation. @@ -64771,7 +64862,7 @@ class Sender { /** * Creates a Sender instance. * - * @param {(net.Socket|tls.Socket)} socket The connection socket + * @param {Duplex} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions * @param {Function} [generateMask] The function used to generate the masking * key @@ -64790,8 +64881,10 @@ class Sender { this._compress = false; this._bufferedBytes = 0; - this._deflating = false; this._queue = []; + this._state = DEFAULT; + this.onerror = NOOP; + this[kWebSocket] = undefined; } /** @@ -64827,7 +64920,24 @@ class Sender { if (options.generateMask) { options.generateMask(mask); } else { - randomFillSync(mask, 0, 4); + if (randomPoolPointer === RANDOM_POOL_SIZE) { + /* istanbul ignore else */ + if (randomPool === undefined) { + // + // This is lazily initialized because server-sent frames must not + // be masked so it may never be used. + // + randomPool = Buffer.alloc(RANDOM_POOL_SIZE); + } + + randomFillSync(randomPool, 0, RANDOM_POOL_SIZE); + randomPoolPointer = 0; + } + + mask[0] = randomPool[randomPoolPointer++]; + mask[1] = randomPool[randomPoolPointer++]; + mask[2] = randomPool[randomPoolPointer++]; + mask[3] = randomPool[randomPoolPointer++]; } skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; @@ -64941,7 +65051,7 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (this._state !== DEFAULT) { this.enqueue([this.dispatch, buf, false, options, cb]); } else { this.sendFrame(Sender.frame(buf, options), cb); @@ -64963,6 +65073,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -64984,7 +65097,13 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(Sender.frame(data, options), cb); @@ -65006,6 +65125,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -65027,7 +65149,13 @@ class Sender { rsv1: false }; - if (this._deflating) { + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(Sender.frame(data, options), cb); @@ -65061,6 +65189,9 @@ class Sender { if (typeof data === 'string') { byteLength = Buffer.byteLength(data); readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; } else { data = toBuffer(data); byteLength = data.length; @@ -65088,40 +65219,94 @@ class Sender { if (options.fin) this._firstFragment = true; - if (perMessageDeflate) { - const opts = { - [kByteLength]: byteLength, - fin: options.fin, - generateMask: this._generateMask, - mask: options.mask, - maskBuffer: this._maskBuffer, - opcode, - readOnly, - rsv1 - }; + const opts = { + [kByteLength]: byteLength, + fin: options.fin, + generateMask: this._generateMask, + mask: options.mask, + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 + }; - if (this._deflating) { - this.enqueue([this.dispatch, data, this._compress, opts, cb]); + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, this._compress, opts, cb]); } else { - this.dispatch(data, this._compress, opts, cb); + this.getBlobData(data, this._compress, opts, cb); } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { - this.sendFrame( - Sender.frame(data, { - [kByteLength]: byteLength, - fin: options.fin, - generateMask: this._generateMask, - mask: options.mask, - maskBuffer: this._maskBuffer, - opcode, - readOnly, - rsv1: false - }), - cb - ); + this.dispatch(data, this._compress, opts, cb); } } + /** + * Gets the contents of a blob as binary data. + * + * @param {Blob} blob The blob + * @param {Boolean} [compress=false] Specifies whether or not to compress + * the data + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + getBlobData(blob, compress, options, cb) { + this._bufferedBytes += options[kByteLength]; + this._state = GET_BLOB_DATA; + + blob + .arrayBuffer() + .then((arrayBuffer) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while the blob was being read' + ); + + // + // `callCallbacks` is called in the next tick to ensure that errors + // that might be thrown in the callbacks behave like errors thrown + // outside the promise chain. + // + process.nextTick(callCallbacks, this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + const data = toBuffer(arrayBuffer); + + if (!compress) { + this._state = DEFAULT; + this.sendFrame(Sender.frame(data, options), cb); + this.dequeue(); + } else { + this.dispatch(data, compress, options, cb); + } + }) + .catch((err) => { + // + // `onError` is called in the next tick for the same reason that + // `callCallbacks` above is. + // + process.nextTick(onError, this, err, cb); + }); + } + /** * Dispatches a message. * @@ -65154,27 +65339,19 @@ class Sender { const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; this._bufferedBytes += options[kByteLength]; - this._deflating = true; + this._state = DEFLATING; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { const err = new Error( 'The socket was closed while data was being compressed' ); - if (typeof cb === 'function') cb(err); - - for (let i = 0; i < this._queue.length; i++) { - const params = this._queue[i]; - const callback = params[params.length - 1]; - - if (typeof callback === 'function') callback(err); - } - + callCallbacks(this, err, cb); return; } this._bufferedBytes -= options[kByteLength]; - this._deflating = false; + this._state = DEFAULT; options.readOnly = false; this.sendFrame(Sender.frame(buf, options), cb); this.dequeue(); @@ -65187,7 +65364,7 @@ class Sender { * @private */ dequeue() { - while (!this._deflating && this._queue.length) { + while (this._state === DEFAULT && this._queue.length) { const params = this._queue.shift(); this._bufferedBytes -= params[3][kByteLength]; @@ -65227,6 +65404,38 @@ class Sender { module.exports = Sender; +/** + * Calls queued callbacks with an error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error to call the callbacks with + * @param {Function} [cb] The first callback + * @private + */ +function callCallbacks(sender, err, cb) { + if (typeof cb === 'function') cb(err); + + for (let i = 0; i < sender._queue.length; i++) { + const params = sender._queue[i]; + const callback = params[params.length - 1]; + + if (typeof callback === 'function') callback(err); + } +} + +/** + * Handles a `Sender` error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error + * @param {Function} [cb] The first pending callback + * @private + */ +function onError(sender, err, cb) { + callCallbacks(sender, err, cb); + sender.onerror(err); +} + /***/ }), @@ -65475,6 +65684,8 @@ module.exports = { parse }; const { isUtf8 } = __nccwpck_require__(14300); +const { hasBlob } = __nccwpck_require__(15949); + // // Allowed token characters: // @@ -65580,7 +65791,27 @@ function _isValidUTF8(buf) { return true; } +/** + * Determines whether a value is a `Blob`. + * + * @param {*} value The value to be tested + * @return {Boolean} `true` if `value` is a `Blob`, else `false` + * @private + */ +function isBlob(value) { + return ( + hasBlob && + typeof value === 'object' && + typeof value.arrayBuffer === 'function' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + (value[Symbol.toStringTag] === 'Blob' || + value[Symbol.toStringTag] === 'File') + ); +} + module.exports = { + isBlob, isValidStatusCode, isValidUTF8: _isValidUTF8, tokenChars @@ -65609,15 +65840,13 @@ if (isUtf8) { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */ const EventEmitter = __nccwpck_require__(82361); const http = __nccwpck_require__(13685); -const https = __nccwpck_require__(95687); -const net = __nccwpck_require__(41808); -const tls = __nccwpck_require__(24404); +const { Duplex } = __nccwpck_require__(12781); const { createHash } = __nccwpck_require__(6113); const extension = __nccwpck_require__(92035); @@ -65642,6 +65871,11 @@ class WebSocketServer extends EventEmitter { * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to @@ -65668,6 +65902,8 @@ class WebSocketServer extends EventEmitter { super(); options = { + allowSynchronousEvents: true, + autoPong: true, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, @@ -65832,8 +66068,7 @@ class WebSocketServer extends EventEmitter { * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public @@ -65842,6 +66077,7 @@ class WebSocketServer extends EventEmitter { socket.on('error', socketOnError); const key = req.headers['sec-websocket-key']; + const upgrade = req.headers.upgrade; const version = +req.headers['sec-websocket-version']; if (req.method !== 'GET') { @@ -65850,13 +66086,13 @@ class WebSocketServer extends EventEmitter { return; } - if (req.headers.upgrade.toLowerCase() !== 'websocket') { + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { const message = 'Invalid Upgrade header'; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } - if (!key || !keyRegex.test(key)) { + if (key === undefined || !keyRegex.test(key)) { const message = 'Missing or invalid Sec-WebSocket-Key header'; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; @@ -65957,8 +66193,7 @@ class WebSocketServer extends EventEmitter { * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket @@ -65990,7 +66225,7 @@ class WebSocketServer extends EventEmitter { `Sec-WebSocket-Accept: ${digest}` ]; - const ws = new this.options.WebSocket(null); + const ws = new this.options.WebSocket(null, undefined, this.options); if (protocols.size) { // @@ -66024,6 +66259,7 @@ class WebSocketServer extends EventEmitter { socket.removeListener('error', socketOnError); ws.setSocket(socket, head, { + allowSynchronousEvents: this.options.allowSynchronousEvents, maxPayload: this.options.maxPayload, skipUTF8Validation: this.options.skipUTF8Validation }); @@ -66088,7 +66324,7 @@ function socketOnError() { /** * Close the connection when preconditions are not fulfilled. * - * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Duplex} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} [message] The HTTP response body * @param {Object} [headers] Additional HTTP response headers @@ -66129,7 +66365,7 @@ function abortHandshake(socket, code, message, headers) { * * @param {WebSocketServer} server The WebSocket server * @param {http.IncomingMessage} req The request object - * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Duplex} socket The socket of the upgrade request * @param {Number} code The HTTP response status code * @param {String} message The HTTP response body * @private @@ -66152,7 +66388,7 @@ function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$", "caughtErrors": "none" }] */ @@ -66162,12 +66398,14 @@ const http = __nccwpck_require__(13685); const net = __nccwpck_require__(41808); const tls = __nccwpck_require__(24404); const { randomBytes, createHash } = __nccwpck_require__(6113); -const { Readable } = __nccwpck_require__(12781); +const { Duplex, Readable } = __nccwpck_require__(12781); const { URL } = __nccwpck_require__(57310); const PerMessageDeflate = __nccwpck_require__(56684); const Receiver = __nccwpck_require__(25066); const Sender = __nccwpck_require__(36947); +const { isBlob } = __nccwpck_require__(86279); + const { BINARY_TYPES, EMPTY_BUFFER, @@ -66212,6 +66450,7 @@ class WebSocket extends EventEmitter { this._closeFrameSent = false; this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; + this._errorEmitted = false; this._extensions = {}; this._paused = false; this._protocol = ''; @@ -66238,14 +66477,14 @@ class WebSocket extends EventEmitter { initAsClient(this, address, protocols, options); } else { + this._autoPong = options.autoPong; this._isServer = true; } } /** - * This deviates from the WHATWG interface since ws doesn't support the - * required default "blob" type (instead we define a custom "nodebuffer" - * type). + * For historical reasons, the custom "nodebuffer" type is used by the default + * instead of "blob". * * @type {String} */ @@ -66343,10 +66582,12 @@ class WebSocket extends EventEmitter { /** * Set up the socket and the internal resources. * - * @param {(net.Socket|tls.Socket)} socket The network socket between the - * server and client + * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Number} [options.maxPayload=0] The maximum allowed message size @@ -66356,6 +66597,7 @@ class WebSocket extends EventEmitter { */ setSocket(socket, head, options) { const receiver = new Receiver({ + allowSynchronousEvents: options.allowSynchronousEvents, binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, @@ -66363,11 +66605,14 @@ class WebSocket extends EventEmitter { skipUTF8Validation: options.skipUTF8Validation }); - this._sender = new Sender(socket, this._extensions, options.generateMask); + const sender = new Sender(socket, this._extensions, options.generateMask); + this._receiver = receiver; + this._sender = sender; this._socket = socket; receiver[kWebSocket] = this; + sender[kWebSocket] = this; socket[kWebSocket] = this; receiver.on('conclude', receiverOnConclude); @@ -66377,8 +66622,13 @@ class WebSocket extends EventEmitter { receiver.on('ping', receiverOnPing); receiver.on('pong', receiverOnPong); - socket.setTimeout(0); - socket.setNoDelay(); + sender.onerror = senderOnError; + + // + // These methods may not be available if `socket` is just a `Duplex`. + // + if (socket.setTimeout) socket.setTimeout(0); + if (socket.setNoDelay) socket.setNoDelay(); if (head.length > 0) socket.unshift(head); @@ -66469,13 +66719,7 @@ class WebSocket extends EventEmitter { } }); - // - // Specify a timeout for the closing handshake to complete. - // - this._closeTimer = setTimeout( - this._socket.destroy.bind(this._socket), - closeTimeout - ); + setCloseTimer(this); } /** @@ -66770,6 +67014,13 @@ module.exports = WebSocket; * @param {(String|URL)} address The URL to which to connect * @param {Array} protocols The subprotocols * @param {Object} [options] Connection options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether any + * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple + * times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping + * @param {Function} [options.finishRequest] A function which can be used to + * customize the headers of each http request before it is sent * @param {Boolean} [options.followRedirects=false] Whether or not to follow * redirects * @param {Function} [options.generateMask] The function used to generate the @@ -66792,6 +67043,8 @@ module.exports = WebSocket; */ function initAsClient(websocket, address, protocols, options) { const opts = { + allowSynchronousEvents: true, + autoPong: true, protocolVersion: protocolVersions[1], maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, @@ -66799,7 +67052,6 @@ function initAsClient(websocket, address, protocols, options) { followRedirects: false, maxRedirects: 10, ...options, - createConnection: undefined, socketPath: undefined, hostname: undefined, protocol: undefined, @@ -66810,6 +67062,8 @@ function initAsClient(websocket, address, protocols, options) { port: undefined }; + websocket._autoPong = opts.autoPong; + if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( `Unsupported protocol version: ${opts.protocolVersion} ` + @@ -66821,24 +67075,30 @@ function initAsClient(websocket, address, protocols, options) { if (address instanceof URL) { parsedUrl = address; - websocket._url = address.href; } else { try { parsedUrl = new URL(address); } catch (e) { throw new SyntaxError(`Invalid URL: ${address}`); } + } - websocket._url = address; + if (parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'ws:'; + } else if (parsedUrl.protocol === 'https:') { + parsedUrl.protocol = 'wss:'; } + websocket._url = parsedUrl.href; + const isSecure = parsedUrl.protocol === 'wss:'; const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; let invalidUrlMessage; if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { invalidUrlMessage = - 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https", or "ws+unix:"'; } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { @@ -66862,7 +67122,8 @@ function initAsClient(websocket, address, protocols, options) { const protocolSet = new Set(); let perMessageDeflate; - opts.createConnection = isSecure ? tlsConnect : netConnect; + opts.createConnection = + opts.createConnection || (isSecure ? tlsConnect : netConnect); opts.defaultPort = opts.defaultPort || defaultPort; opts.port = parsedUrl.port || defaultPort; opts.host = parsedUrl.hostname.startsWith('[') @@ -66952,8 +67213,8 @@ function initAsClient(websocket, address, protocols, options) { ? opts.socketPath === websocket._originalHostOrSocketPath : false : websocket._originalIpc - ? false - : parsedUrl.host === websocket._originalHostOrSocketPath; + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; if (!isSameHost || (websocket._originalSecure && !isSecure)) { // @@ -67058,7 +67319,9 @@ function initAsClient(websocket, address, protocols, options) { req = websocket._req = null; - if (res.headers.upgrade.toLowerCase() !== 'websocket') { + const upgrade = res.headers.upgrade; + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { abortHandshake(websocket, socket, 'Invalid Upgrade header'); return; } @@ -67137,6 +67400,7 @@ function initAsClient(websocket, address, protocols, options) { } websocket.setSocket(socket, head, { + allowSynchronousEvents: opts.allowSynchronousEvents, generateMask: opts.generateMask, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation @@ -67159,6 +67423,11 @@ function initAsClient(websocket, address, protocols, options) { */ function emitErrorAndClose(websocket, err) { websocket._readyState = WebSocket.CLOSING; + // + // The following assignment is practically useless and is done only for + // consistency. + // + websocket._errorEmitted = true; websocket.emit('error', err); websocket.emitClose(); } @@ -67239,7 +67508,7 @@ function abortHandshake(websocket, stream, message) { */ function sendAfterClose(websocket, data, cb) { if (data) { - const length = toBuffer(data).length; + const length = isBlob(data) ? data.size : toBuffer(data).length; // // The `_bufferedAmount` property is used only when the peer is a client and @@ -67315,7 +67584,10 @@ function receiverOnError(err) { websocket.close(err[kStatusCode]); } - websocket.emit('error', err); + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } } /** @@ -67347,7 +67619,7 @@ function receiverOnMessage(data, isBinary) { function receiverOnPing(data) { const websocket = this[kWebSocket]; - websocket.pong(data, !websocket._isServer, NOOP); + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); websocket.emit('ping', data); } @@ -67372,7 +67644,48 @@ function resume(stream) { } /** - * The listener of the `net.Socket` `'close'` event. + * The `Sender` error event handler. + * + * @param {Error} The error + * @private + */ +function senderOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket.readyState === WebSocket.CLOSED) return; + if (websocket.readyState === WebSocket.OPEN) { + websocket._readyState = WebSocket.CLOSING; + setCloseTimer(websocket); + } + + // + // `socket.end()` is used instead of `socket.destroy()` to allow the other + // peer to finish sending queued data. There is no need to set a timer here + // because `CLOSING` means that it is already set or not needed. + // + this._socket.end(); + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * Set a timer to destroy the underlying raw socket of a WebSocket. + * + * @param {WebSocket} websocket The WebSocket instance + * @private + */ +function setCloseTimer(websocket) { + websocket._closeTimer = setTimeout( + websocket._socket.destroy.bind(websocket._socket), + closeTimeout + ); +} + +/** + * The listener of the socket `'close'` event. * * @private */ @@ -67423,7 +67736,7 @@ function socketOnClose() { } /** - * The listener of the `net.Socket` `'data'` event. + * The listener of the socket `'data'` event. * * @param {Buffer} chunk A chunk of data * @private @@ -67435,7 +67748,7 @@ function socketOnData(chunk) { } /** - * The listener of the `net.Socket` `'end'` event. + * The listener of the socket `'end'` event. * * @private */ @@ -67448,7 +67761,7 @@ function socketOnEnd() { } /** - * The listener of the `net.Socket` `'error'` event. + * The listener of the socket `'error'` event. * * @private */ diff --git a/src/cron.test.ts b/src/cron.test.ts index afefec5..880ac1e 100644 --- a/src/cron.test.ts +++ b/src/cron.test.ts @@ -1,6 +1,6 @@ import { getAllRFCRemarks } from "./cron"; -const TIMEOUT = 180_000; +const TIMEOUT = 240_000; describe("RFC Listing test", () => { test( diff --git a/src/find-referendum.test.ts b/src/find-referendum.test.ts index c283eb7..4bb814e 100644 --- a/src/find-referendum.test.ts +++ b/src/find-referendum.test.ts @@ -19,7 +19,7 @@ describe("findReferendum", () => { }); expect(result).toEqual("approved"); - }); + }, 10_000); test.skip("Finds the (inlined) 0014 referendum", async () => { /** diff --git a/yarn.lock b/yarn.lock index e7a4b1e..4bd71ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4697,9 +4697,9 @@ write-file-atomic@^4.0.2: signal-exit "^3.0.7" ws@^8.13.0, ws@^8.8.1: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== y18n@^5.0.5: version "5.0.8"