diff --git a/node_modules/ws/lib/websocket.js b/node_modules/ws/lib/websocket.js index ce6f34c..2613af1 100644 --- a/node_modules/ws/lib/websocket.js +++ b/node_modules/ws/lib/websocket.js @@ -1,3 +1,5 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + 'use strict'; const EventEmitter = require('events'); @@ -6,6 +8,7 @@ const http = require('http'); const net = require('net'); const tls = require('tls'); const { randomBytes, createHash } = require('crypto'); +const { Readable } = require('stream'); const { URL } = require('url'); const PerMessageDeflate = require('./permessage-deflate'); @@ -36,23 +39,22 @@ class WebSocket extends EventEmitter { /** * Create a new `WebSocket`. * - * @param {(String|url.URL)} address The URL to which to connect - * @param {(String|String[])} protocols The subprotocols - * @param {Object} options Connection options + * @param {(String|URL)} address The URL to which to connect + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options */ constructor(address, protocols, options) { super(); - this.readyState = WebSocket.CONNECTING; - this.protocol = ''; - this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; this._closeMessage = ''; this._closeTimer = null; - this._closeCode = 1006; this._extensions = {}; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; this._receiver = null; this._sender = null; this._socket = null; @@ -75,19 +77,6 @@ class WebSocket extends EventEmitter { } } - get CONNECTING() { - return WebSocket.CONNECTING; - } - get CLOSING() { - return WebSocket.CLOSING; - } - get CLOSED() { - return WebSocket.CLOSED; - } - get OPEN() { - return WebSocket.OPEN; - } - /** * This deviates from the WHATWG interface since ws doesn't support the * required default "blob" type (instead we define a custom "nodebuffer" @@ -126,17 +115,83 @@ class WebSocket extends EventEmitter { return Object.keys(this._extensions).join(); } + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return undefined; + } + + /* istanbul ignore next */ + set onclose(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return undefined; + } + + /* istanbul ignore next */ + set onerror(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return undefined; + } + + /* istanbul ignore next */ + set onopen(listener) {} + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return undefined; + } + + /* istanbul ignore next */ + set onmessage(listener) {} + + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + /** * Set up the socket and the internal resources. * - * @param {net.Socket} socket The network socket between the server and client + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client * @param {Buffer} head The first packet of the upgraded stream - * @param {Number} maxPayload The maximum allowed message size + * @param {Number} [maxPayload=0] The maximum allowed message size * @private */ setSocket(socket, head, maxPayload) { const receiver = new Receiver( - this._binaryType, + this.binaryType, this._extensions, this._isServer, maxPayload @@ -166,7 +221,7 @@ class WebSocket extends EventEmitter { socket.on('end', socketOnEnd); socket.on('error', socketOnError); - this.readyState = WebSocket.OPEN; + this._readyState = WebSocket.OPEN; this.emit('open'); } @@ -177,7 +232,7 @@ class WebSocket extends EventEmitter { */ emitClose() { if (!this._socket) { - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); return; } @@ -187,7 +242,7 @@ class WebSocket extends EventEmitter { } this._receiver.removeAllListeners(); - this.readyState = WebSocket.CLOSED; + this._readyState = WebSocket.CLOSED; this.emit('close', this._closeCode, this._closeMessage); } @@ -206,8 +261,8 @@ class WebSocket extends EventEmitter { * - - - - -|fin|<---------------------+ * +---+ * - * @param {Number} code Status code explaining why the connection is closing - * @param {String} data A string explaining why the connection is closing + * @param {Number} [code] Status code explaining why the connection is closing + * @param {String} [data] A string explaining why the connection is closing * @public */ close(code, data) { @@ -218,11 +273,17 @@ class WebSocket extends EventEmitter { } if (this.readyState === WebSocket.CLOSING) { - if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + return; } - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._sender.close(code, data, !this._isServer, (err) => { // // This error is handled by the `'error'` listener on the socket. We only @@ -231,7 +292,13 @@ class WebSocket extends EventEmitter { if (err) return; this._closeFrameSent = true; - if (this._closeFrameReceived) this._socket.end(); + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } }); // @@ -246,9 +313,9 @@ class WebSocket extends EventEmitter { /** * Send a ping. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the ping is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent * @public */ ping(data, mask, cb) { @@ -278,9 +345,9 @@ class WebSocket extends EventEmitter { /** * Send a pong. * - * @param {*} data The data to send - * @param {Boolean} mask Indicates whether or not to mask `data` - * @param {Function} cb Callback which is executed when the pong is sent + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent * @public */ pong(data, mask, cb) { @@ -311,13 +378,15 @@ class WebSocket extends EventEmitter { * Send a data message. * * @param {*} data The message to send - * @param {Object} options Options object - * @param {Boolean} options.compress Specifies whether or not to compress + * @param {Object} [options] Options object + * @param {Boolean} [options.compress] Specifies whether or not to compress * `data` - * @param {Boolean} options.binary Specifies whether `data` is binary or text - * @param {Boolean} options.fin Specifies whether the fragment is the last one - * @param {Boolean} options.mask Specifies whether or not to mask `data` - * @param {Function} cb Callback which is executed when data is written out + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out * @public */ send(data, options, cb) { @@ -365,14 +434,93 @@ class WebSocket extends EventEmitter { } if (this._socket) { - this.readyState = WebSocket.CLOSING; + this._readyState = WebSocket.CLOSING; this._socket.destroy(); } } } -readyStates.forEach((readyState, i) => { - WebSocket[readyState] = i; +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); }); // @@ -381,12 +529,7 @@ readyStates.forEach((readyState, i) => { // ['open', 'error', 'close', 'message'].forEach((method) => { Object.defineProperty(WebSocket.prototype, `on${method}`, { - /** - * Return the listener of the event. - * - * @return {(Function|undefined)} The event listener or `undefined` - * @public - */ + enumerable: true, get() { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -395,12 +538,6 @@ readyStates.forEach((readyState, i) => { return undefined; }, - /** - * Add a listener for the event. - * - * @param {Function} listener The listener to add - * @public - */ set(listener) { const listeners = this.listeners(method); for (let i = 0; i < listeners.length; i++) { @@ -423,20 +560,23 @@ module.exports = WebSocket; * Initialize a WebSocket client. * * @param {WebSocket} websocket The client to initialize - * @param {(String|url.URL)} address The URL to which to connect - * @param {String} protocols The subprotocols - * @param {Object} options Connection options - * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable + * @param {(String|URL)} address The URL to which to connect + * @param {String} [protocols] The subprotocols + * @param {Object} [options] Connection options + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable * permessage-deflate - * @param {Number} options.handshakeTimeout Timeout in milliseconds for the + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the * handshake request - * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` - * header - * @param {String} options.origin Value of the `Origin` or + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {String} [options.origin] Value of the `Origin` or * `Sec-WebSocket-Origin` header - * @param {Number} options.maxPayload The maximum allowed message size - * @param {Boolean} options.followRedirects Whether or not to follow redirects - * @param {Number} options.maxRedirects The maximum number of redirects allowed + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed * @private */ function initAsClient(websocket, address, protocols, options) { @@ -469,16 +609,23 @@ function initAsClient(websocket, address, protocols, options) { if (address instanceof URL) { parsedUrl = address; - websocket.url = address.href; + websocket._url = address.href; } else { parsedUrl = new URL(address); - websocket.url = address; + websocket._url = address; } const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { - throw new Error(`Invalid URL: ${websocket.url}`); + const err = new Error(`Invalid URL: ${websocket.url}`); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } } const isSecure = @@ -544,12 +691,10 @@ function initAsClient(websocket, address, protocols, options) { } req.on('error', (err) => { - if (websocket._req.aborted) return; + if (req === null || req.aborted) return; req = websocket._req = null; - websocket.readyState = WebSocket.CLOSING; - websocket.emit('error', err); - websocket.emitClose(); + emitErrorAndClose(websocket, err); }); req.on('response', (res) => { @@ -569,7 +714,14 @@ function initAsClient(websocket, address, protocols, options) { req.abort(); - const addr = new URL(location, address); + let addr; + + try { + addr = new URL(location, address); + } catch (err) { + emitErrorAndClose(websocket, err); + return; + } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit('unexpected-response', req, res)) { @@ -618,32 +770,72 @@ function initAsClient(websocket, address, protocols, options) { return; } - if (serverProt) websocket.protocol = serverProt; + if (serverProt) websocket._protocol = serverProt; - if (perMessageDeflate) { - try { - const extensions = parse(res.headers['sec-websocket-extensions']); + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; - if (extensions[PerMessageDeflate.extensionName]) { - perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); - websocket._extensions[ - PerMessageDeflate.extensionName - ] = perMessageDeflate; - } - } catch (err) { - abortHandshake( - websocket, - socket, - 'Invalid Sec-WebSocket-Extensions header' - ); + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); return; } + + let extensions; + + try { + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if (extensionNames.length) { + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = + 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; + } } websocket.setSocket(socket, head, opts.maxPayload); }); } +/** + * Emit the `'error'` and `'close'` event. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); +} + /** * Create a `net.Socket` and initiate a connection. * @@ -667,7 +859,7 @@ function tlsConnect(options) { options.path = undefined; if (!options.servername && options.servername !== '') { - options.servername = options.host; + options.servername = net.isIP(options.host) ? '' : options.host; } return tls.connect(options); @@ -677,19 +869,29 @@ function tlsConnect(options) { * Abort the handshake and emit an error. * * @param {WebSocket} websocket The WebSocket instance - * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the - * socket to destroy + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy * @param {String} message The error message * @private */ function abortHandshake(websocket, stream, message) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; const err = new Error(message); Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + stream.once('abort', websocket.emitClose.bind(websocket)); websocket.emit('error', err); } else { @@ -704,8 +906,8 @@ function abortHandshake(websocket, stream, message) { * when the `readyState` attribute is `CLOSING` or `CLOSED`. * * @param {WebSocket} websocket The WebSocket instance - * @param {*} data The data to send - * @param {Function} cb Callback + * @param {*} [data] The data to send + * @param {Function} [cb] Callback * @private */ function sendAfterClose(websocket, data, cb) { @@ -741,13 +943,15 @@ function sendAfterClose(websocket, data, cb) { function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); - websocket._socket.resume(); - websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + if (code === 1005) websocket.close(); else websocket.close(code, reason); } @@ -770,12 +974,19 @@ function receiverOnDrain() { function receiverOnError(err) { const websocket = this[kWebSocket]; - websocket._socket.removeListener('data', socketOnData); + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } - websocket.readyState = WebSocket.CLOSING; - websocket._closeCode = err[kStatusCode]; websocket.emit('error', err); - websocket._socket.destroy(); } /** @@ -820,6 +1031,16 @@ function receiverOnPong(data) { this[kWebSocket].emit('pong', data); } +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + /** * The listener of the `net.Socket` `'close'` event. * @@ -829,9 +1050,12 @@ function socketOnClose() { const websocket = this[kWebSocket]; this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); this.removeListener('end', socketOnEnd); - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; + + let chunk; // // The close frame might not have been received or the `'end'` event emitted, @@ -840,13 +1064,19 @@ function socketOnClose() { // it. If the readable side of the socket is in flowing mode then there is no // buffered data as everything has been already written and `readable.read()` // will return `null`. If instead, the socket is paused, any possible buffered - // data will be read as a single chunk and emitted synchronously in a single - // `'data'` event. + // data will be read as a single chunk. // - websocket._socket.read(); + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + websocket._receiver.end(); - this.removeListener('data', socketOnData); this[kWebSocket] = undefined; clearTimeout(websocket._closeTimer); @@ -882,7 +1112,7 @@ function socketOnData(chunk) { function socketOnEnd() { const websocket = this[kWebSocket]; - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; websocket._receiver.end(); this.end(); } @@ -899,7 +1129,7 @@ function socketOnError() { this.on('error', NOOP); if (websocket) { - websocket.readyState = WebSocket.CLOSING; + websocket._readyState = WebSocket.CLOSING; this.destroy(); } }