diff options
| author | Joel Kronqvist <joel.h.kronqvist@gmail.com> | 2022-03-05 19:02:27 +0200 | 
|---|---|---|
| committer | Joel Kronqvist <joel.h.kronqvist@gmail.com> | 2022-03-05 19:02:27 +0200 | 
| commit | 5d309ff52cd399a6b71968a6b9a70c8ac0b98981 (patch) | |
| tree | 360f7eb50f956e2367ef38fa1fc6ac7ac5258042 /node_modules/ws/lib | |
| parent | b500a50f1b97d93c98b36ed9a980f8188d648147 (diff) | |
| download | LYLLRuoka-5d309ff52cd399a6b71968a6b9a70c8ac0b98981.tar.gz LYLLRuoka-5d309ff52cd399a6b71968a6b9a70c8ac0b98981.zip  | |
Added node_modules for the updating to work properly.
Diffstat (limited to 'node_modules/ws/lib')
| -rw-r--r-- | node_modules/ws/lib/buffer-util.js | 129 | ||||
| -rw-r--r-- | node_modules/ws/lib/constants.js | 10 | ||||
| -rw-r--r-- | node_modules/ws/lib/event-target.js | 184 | ||||
| -rw-r--r-- | node_modules/ws/lib/extension.js | 223 | ||||
| -rw-r--r-- | node_modules/ws/lib/limiter.js | 55 | ||||
| -rw-r--r-- | node_modules/ws/lib/permessage-deflate.js | 518 | ||||
| -rw-r--r-- | node_modules/ws/lib/receiver.js | 607 | ||||
| -rw-r--r-- | node_modules/ws/lib/sender.js | 409 | ||||
| -rw-r--r-- | node_modules/ws/lib/stream.js | 180 | ||||
| -rw-r--r-- | node_modules/ws/lib/validation.js | 104 | ||||
| -rw-r--r-- | node_modules/ws/lib/websocket-server.js | 447 | ||||
| -rw-r--r-- | node_modules/ws/lib/websocket.js | 1174 | 
12 files changed, 4040 insertions, 0 deletions
diff --git a/node_modules/ws/lib/buffer-util.js b/node_modules/ws/lib/buffer-util.js new file mode 100644 index 0000000..6fd84c3 --- /dev/null +++ b/node_modules/ws/lib/buffer-util.js @@ -0,0 +1,129 @@ +'use strict'; + +const { EMPTY_BUFFER } = require('./constants'); + +/** + * Merges an array of buffers into a new buffer. + * + * @param {Buffer[]} list The array of buffers to concat + * @param {Number} totalLength The total length of buffers in the list + * @return {Buffer} The resulting buffer + * @public + */ +function concat(list, totalLength) { +  if (list.length === 0) return EMPTY_BUFFER; +  if (list.length === 1) return list[0]; + +  const target = Buffer.allocUnsafe(totalLength); +  let offset = 0; + +  for (let i = 0; i < list.length; i++) { +    const buf = list[i]; +    target.set(buf, offset); +    offset += buf.length; +  } + +  if (offset < totalLength) return target.slice(0, offset); + +  return target; +} + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +function _mask(source, mask, output, offset, length) { +  for (let i = 0; i < length; i++) { +    output[offset + i] = source[i] ^ mask[i & 3]; +  } +} + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +function _unmask(buffer, mask) { +  // Required until https://github.com/nodejs/node/issues/9006 is resolved. +  const length = buffer.length; +  for (let i = 0; i < length; i++) { +    buffer[i] ^= mask[i & 3]; +  } +} + +/** + * Converts a buffer to an `ArrayBuffer`. + * + * @param {Buffer} buf The buffer to convert + * @return {ArrayBuffer} Converted buffer + * @public + */ +function toArrayBuffer(buf) { +  if (buf.byteLength === buf.buffer.byteLength) { +    return buf.buffer; +  } + +  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +/** + * Converts `data` to a `Buffer`. + * + * @param {*} data The data to convert + * @return {Buffer} The buffer + * @throws {TypeError} + * @public + */ +function toBuffer(data) { +  toBuffer.readOnly = true; + +  if (Buffer.isBuffer(data)) return data; + +  let buf; + +  if (data instanceof ArrayBuffer) { +    buf = Buffer.from(data); +  } else if (ArrayBuffer.isView(data)) { +    buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); +  } else { +    buf = Buffer.from(data); +    toBuffer.readOnly = false; +  } + +  return buf; +} + +try { +  const bufferUtil = require('bufferutil'); +  const bu = bufferUtil.BufferUtil || bufferUtil; + +  module.exports = { +    concat, +    mask(source, mask, output, offset, length) { +      if (length < 48) _mask(source, mask, output, offset, length); +      else bu.mask(source, mask, output, offset, length); +    }, +    toArrayBuffer, +    toBuffer, +    unmask(buffer, mask) { +      if (buffer.length < 32) _unmask(buffer, mask); +      else bu.unmask(buffer, mask); +    } +  }; +} catch (e) /* istanbul ignore next */ { +  module.exports = { +    concat, +    mask: _mask, +    toArrayBuffer, +    toBuffer, +    unmask: _unmask +  }; +} diff --git a/node_modules/ws/lib/constants.js b/node_modules/ws/lib/constants.js new file mode 100644 index 0000000..4082981 --- /dev/null +++ b/node_modules/ws/lib/constants.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { +  BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], +  GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', +  kStatusCode: Symbol('status-code'), +  kWebSocket: Symbol('websocket'), +  EMPTY_BUFFER: Buffer.alloc(0), +  NOOP: () => {} +}; diff --git a/node_modules/ws/lib/event-target.js b/node_modules/ws/lib/event-target.js new file mode 100644 index 0000000..a6fbe72 --- /dev/null +++ b/node_modules/ws/lib/event-target.js @@ -0,0 +1,184 @@ +'use strict'; + +/** + * Class representing an event. + * + * @private + */ +class Event { +  /** +   * Create a new `Event`. +   * +   * @param {String} type The name of the event +   * @param {Object} target A reference to the target to which the event was +   *     dispatched +   */ +  constructor(type, target) { +    this.target = target; +    this.type = type; +  } +} + +/** + * Class representing a message event. + * + * @extends Event + * @private + */ +class MessageEvent extends Event { +  /** +   * Create a new `MessageEvent`. +   * +   * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data +   * @param {WebSocket} target A reference to the target to which the event was +   *     dispatched +   */ +  constructor(data, target) { +    super('message', target); + +    this.data = data; +  } +} + +/** + * Class representing a close event. + * + * @extends Event + * @private + */ +class CloseEvent extends Event { +  /** +   * Create a new `CloseEvent`. +   * +   * @param {Number} code The status code explaining why the connection is being +   *     closed +   * @param {String} reason A human-readable string explaining why the +   *     connection is closing +   * @param {WebSocket} target A reference to the target to which the event was +   *     dispatched +   */ +  constructor(code, reason, target) { +    super('close', target); + +    this.wasClean = target._closeFrameReceived && target._closeFrameSent; +    this.reason = reason; +    this.code = code; +  } +} + +/** + * Class representing an open event. + * + * @extends Event + * @private + */ +class OpenEvent extends Event { +  /** +   * Create a new `OpenEvent`. +   * +   * @param {WebSocket} target A reference to the target to which the event was +   *     dispatched +   */ +  constructor(target) { +    super('open', target); +  } +} + +/** + * Class representing an error event. + * + * @extends Event + * @private + */ +class ErrorEvent extends Event { +  /** +   * Create a new `ErrorEvent`. +   * +   * @param {Object} error The error that generated this event +   * @param {WebSocket} target A reference to the target to which the event was +   *     dispatched +   */ +  constructor(error, target) { +    super('error', target); + +    this.message = error.message; +    this.error = error; +  } +} + +/** + * This provides methods for emulating the `EventTarget` interface. It's not + * meant to be used directly. + * + * @mixin + */ +const EventTarget = { +  /** +   * Register an event listener. +   * +   * @param {String} type A string representing the event type to listen for +   * @param {Function} listener The listener to add +   * @param {Object} [options] An options object specifies characteristics about +   *     the event listener +   * @param {Boolean} [options.once=false] A `Boolean`` indicating that the +   *     listener should be invoked at most once after being added. If `true`, +   *     the listener would be automatically removed when invoked. +   * @public +   */ +  addEventListener(type, listener, options) { +    if (typeof listener !== 'function') return; + +    function onMessage(data) { +      listener.call(this, new MessageEvent(data, this)); +    } + +    function onClose(code, message) { +      listener.call(this, new CloseEvent(code, message, this)); +    } + +    function onError(error) { +      listener.call(this, new ErrorEvent(error, this)); +    } + +    function onOpen() { +      listener.call(this, new OpenEvent(this)); +    } + +    const method = options && options.once ? 'once' : 'on'; + +    if (type === 'message') { +      onMessage._listener = listener; +      this[method](type, onMessage); +    } else if (type === 'close') { +      onClose._listener = listener; +      this[method](type, onClose); +    } else if (type === 'error') { +      onError._listener = listener; +      this[method](type, onError); +    } else if (type === 'open') { +      onOpen._listener = listener; +      this[method](type, onOpen); +    } else { +      this[method](type, listener); +    } +  }, + +  /** +   * Remove an event listener. +   * +   * @param {String} type A string representing the event type to remove +   * @param {Function} listener The listener to remove +   * @public +   */ +  removeEventListener(type, listener) { +    const listeners = this.listeners(type); + +    for (let i = 0; i < listeners.length; i++) { +      if (listeners[i] === listener || listeners[i]._listener === listener) { +        this.removeListener(type, listeners[i]); +      } +    } +  } +}; + +module.exports = EventTarget; diff --git a/node_modules/ws/lib/extension.js b/node_modules/ws/lib/extension.js new file mode 100644 index 0000000..87a4213 --- /dev/null +++ b/node_modules/ws/lib/extension.js @@ -0,0 +1,223 @@ +'use strict'; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ +  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 +  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 +  0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 +  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 +  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 +  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 +  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 +  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + +/** + * Adds an offer to the map of extension offers or a parameter to the map of + * parameters. + * + * @param {Object} dest The map of extension offers or parameters + * @param {String} name The extension or parameter name + * @param {(Object|Boolean|String)} elem The extension parameters or the + *     parameter value + * @private + */ +function push(dest, name, elem) { +  if (dest[name] === undefined) dest[name] = [elem]; +  else dest[name].push(elem); +} + +/** + * Parses the `Sec-WebSocket-Extensions` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed object + * @public + */ +function parse(header) { +  const offers = Object.create(null); + +  if (header === undefined || header === '') return offers; + +  let params = Object.create(null); +  let mustUnescape = false; +  let isEscaping = false; +  let inQuotes = false; +  let extensionName; +  let paramName; +  let start = -1; +  let end = -1; +  let i = 0; + +  for (; i < header.length; i++) { +    const code = header.charCodeAt(i); + +    if (extensionName === undefined) { +      if (end === -1 && tokenChars[code] === 1) { +        if (start === -1) start = i; +      } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) { +        if (end === -1 && start !== -1) end = i; +      } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { +        if (start === -1) { +          throw new SyntaxError(`Unexpected character at index ${i}`); +        } + +        if (end === -1) end = i; +        const name = header.slice(start, end); +        if (code === 0x2c) { +          push(offers, name, params); +          params = Object.create(null); +        } else { +          extensionName = name; +        } + +        start = end = -1; +      } else { +        throw new SyntaxError(`Unexpected character at index ${i}`); +      } +    } else if (paramName === undefined) { +      if (end === -1 && tokenChars[code] === 1) { +        if (start === -1) start = i; +      } else if (code === 0x20 || code === 0x09) { +        if (end === -1 && start !== -1) end = i; +      } else if (code === 0x3b || code === 0x2c) { +        if (start === -1) { +          throw new SyntaxError(`Unexpected character at index ${i}`); +        } + +        if (end === -1) end = i; +        push(params, header.slice(start, end), true); +        if (code === 0x2c) { +          push(offers, extensionName, params); +          params = Object.create(null); +          extensionName = undefined; +        } + +        start = end = -1; +      } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { +        paramName = header.slice(start, i); +        start = end = -1; +      } else { +        throw new SyntaxError(`Unexpected character at index ${i}`); +      } +    } else { +      // +      // The value of a quoted-string after unescaping must conform to the +      // token ABNF, so only token characters are valid. +      // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 +      // +      if (isEscaping) { +        if (tokenChars[code] !== 1) { +          throw new SyntaxError(`Unexpected character at index ${i}`); +        } +        if (start === -1) start = i; +        else if (!mustUnescape) mustUnescape = true; +        isEscaping = false; +      } else if (inQuotes) { +        if (tokenChars[code] === 1) { +          if (start === -1) start = i; +        } else if (code === 0x22 /* '"' */ && start !== -1) { +          inQuotes = false; +          end = i; +        } else if (code === 0x5c /* '\' */) { +          isEscaping = true; +        } else { +          throw new SyntaxError(`Unexpected character at index ${i}`); +        } +      } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { +        inQuotes = true; +      } else if (end === -1 && tokenChars[code] === 1) { +        if (start === -1) start = i; +      } else if (start !== -1 && (code === 0x20 || code === 0x09)) { +        if (end === -1) end = i; +      } else if (code === 0x3b || code === 0x2c) { +        if (start === -1) { +          throw new SyntaxError(`Unexpected character at index ${i}`); +        } + +        if (end === -1) end = i; +        let value = header.slice(start, end); +        if (mustUnescape) { +          value = value.replace(/\\/g, ''); +          mustUnescape = false; +        } +        push(params, paramName, value); +        if (code === 0x2c) { +          push(offers, extensionName, params); +          params = Object.create(null); +          extensionName = undefined; +        } + +        paramName = undefined; +        start = end = -1; +      } else { +        throw new SyntaxError(`Unexpected character at index ${i}`); +      } +    } +  } + +  if (start === -1 || inQuotes) { +    throw new SyntaxError('Unexpected end of input'); +  } + +  if (end === -1) end = i; +  const token = header.slice(start, end); +  if (extensionName === undefined) { +    push(offers, token, params); +  } else { +    if (paramName === undefined) { +      push(params, token, true); +    } else if (mustUnescape) { +      push(params, paramName, token.replace(/\\/g, '')); +    } else { +      push(params, paramName, token); +    } +    push(offers, extensionName, params); +  } + +  return offers; +} + +/** + * Builds the `Sec-WebSocket-Extensions` header field value. + * + * @param {Object} extensions The map of extensions and parameters to format + * @return {String} A string representing the given object + * @public + */ +function format(extensions) { +  return Object.keys(extensions) +    .map((extension) => { +      let configurations = extensions[extension]; +      if (!Array.isArray(configurations)) configurations = [configurations]; +      return configurations +        .map((params) => { +          return [extension] +            .concat( +              Object.keys(params).map((k) => { +                let values = params[k]; +                if (!Array.isArray(values)) values = [values]; +                return values +                  .map((v) => (v === true ? k : `${k}=${v}`)) +                  .join('; '); +              }) +            ) +            .join('; '); +        }) +        .join(', '); +    }) +    .join(', '); +} + +module.exports = { format, parse }; diff --git a/node_modules/ws/lib/limiter.js b/node_modules/ws/lib/limiter.js new file mode 100644 index 0000000..3fd3578 --- /dev/null +++ b/node_modules/ws/lib/limiter.js @@ -0,0 +1,55 @@ +'use strict'; + +const kDone = Symbol('kDone'); +const kRun = Symbol('kRun'); + +/** + * A very simple job queue with adjustable concurrency. Adapted from + * https://github.com/STRML/async-limiter + */ +class Limiter { +  /** +   * Creates a new `Limiter`. +   * +   * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed +   *     to run concurrently +   */ +  constructor(concurrency) { +    this[kDone] = () => { +      this.pending--; +      this[kRun](); +    }; +    this.concurrency = concurrency || Infinity; +    this.jobs = []; +    this.pending = 0; +  } + +  /** +   * Adds a job to the queue. +   * +   * @param {Function} job The job to run +   * @public +   */ +  add(job) { +    this.jobs.push(job); +    this[kRun](); +  } + +  /** +   * Removes a job from the queue and runs it if possible. +   * +   * @private +   */ +  [kRun]() { +    if (this.pending === this.concurrency) return; + +    if (this.jobs.length) { +      const job = this.jobs.shift(); + +      this.pending++; +      job(this[kDone]); +    } +  } +} + +module.exports = Limiter; diff --git a/node_modules/ws/lib/permessage-deflate.js b/node_modules/ws/lib/permessage-deflate.js new file mode 100644 index 0000000..ce91784 --- /dev/null +++ b/node_modules/ws/lib/permessage-deflate.js @@ -0,0 +1,518 @@ +'use strict'; + +const zlib = require('zlib'); + +const bufferUtil = require('./buffer-util'); +const Limiter = require('./limiter'); +const { kStatusCode, NOOP } = require('./constants'); + +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); +const kPerMessageDeflate = Symbol('permessage-deflate'); +const kTotalLength = Symbol('total-length'); +const kCallback = Symbol('callback'); +const kBuffers = Symbol('buffers'); +const kError = Symbol('error'); + +// +// We limit zlib concurrency, which prevents severe memory fragmentation +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 +// and https://github.com/websockets/ws/issues/1202 +// +// Intentionally global; it's the global thread pool that's an issue. +// +let zlibLimiter; + +/** + * permessage-deflate implementation. + */ +class PerMessageDeflate { +  /** +   * Creates a PerMessageDeflate instance. +   * +   * @param {Object} [options] Configuration options +   * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept +   *     disabling of server context takeover +   * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ +   *     acknowledge disabling of client context takeover +   * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the +   *     use of a custom server window size +   * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support +   *     for, or request, a custom client window size +   * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on +   *     deflate +   * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on +   *     inflate +   * @param {Number} [options.threshold=1024] Size (in bytes) below which +   *     messages should not be compressed +   * @param {Number} [options.concurrencyLimit=10] The number of concurrent +   *     calls to zlib +   * @param {Boolean} [isServer=false] Create the instance in either server or +   *     client mode +   * @param {Number} [maxPayload=0] The maximum allowed message length +   */ +  constructor(options, isServer, maxPayload) { +    this._maxPayload = maxPayload | 0; +    this._options = options || {}; +    this._threshold = +      this._options.threshold !== undefined ? this._options.threshold : 1024; +    this._isServer = !!isServer; +    this._deflate = null; +    this._inflate = null; + +    this.params = null; + +    if (!zlibLimiter) { +      const concurrency = +        this._options.concurrencyLimit !== undefined +          ? this._options.concurrencyLimit +          : 10; +      zlibLimiter = new Limiter(concurrency); +    } +  } + +  /** +   * @type {String} +   */ +  static get extensionName() { +    return 'permessage-deflate'; +  } + +  /** +   * Create an extension negotiation offer. +   * +   * @return {Object} Extension parameters +   * @public +   */ +  offer() { +    const params = {}; + +    if (this._options.serverNoContextTakeover) { +      params.server_no_context_takeover = true; +    } +    if (this._options.clientNoContextTakeover) { +      params.client_no_context_takeover = true; +    } +    if (this._options.serverMaxWindowBits) { +      params.server_max_window_bits = this._options.serverMaxWindowBits; +    } +    if (this._options.clientMaxWindowBits) { +      params.client_max_window_bits = this._options.clientMaxWindowBits; +    } else if (this._options.clientMaxWindowBits == null) { +      params.client_max_window_bits = true; +    } + +    return params; +  } + +  /** +   * Accept an extension negotiation offer/response. +   * +   * @param {Array} configurations The extension negotiation offers/reponse +   * @return {Object} Accepted configuration +   * @public +   */ +  accept(configurations) { +    configurations = this.normalizeParams(configurations); + +    this.params = this._isServer +      ? this.acceptAsServer(configurations) +      : this.acceptAsClient(configurations); + +    return this.params; +  } + +  /** +   * Releases all resources used by the extension. +   * +   * @public +   */ +  cleanup() { +    if (this._inflate) { +      this._inflate.close(); +      this._inflate = null; +    } + +    if (this._deflate) { +      const callback = this._deflate[kCallback]; + +      this._deflate.close(); +      this._deflate = null; + +      if (callback) { +        callback( +          new Error( +            'The deflate stream was closed while data was being processed' +          ) +        ); +      } +    } +  } + +  /** +   *  Accept an extension negotiation offer. +   * +   * @param {Array} offers The extension negotiation offers +   * @return {Object} Accepted configuration +   * @private +   */ +  acceptAsServer(offers) { +    const opts = this._options; +    const accepted = offers.find((params) => { +      if ( +        (opts.serverNoContextTakeover === false && +          params.server_no_context_takeover) || +        (params.server_max_window_bits && +          (opts.serverMaxWindowBits === false || +            (typeof opts.serverMaxWindowBits === 'number' && +              opts.serverMaxWindowBits > params.server_max_window_bits))) || +        (typeof opts.clientMaxWindowBits === 'number' && +          !params.client_max_window_bits) +      ) { +        return false; +      } + +      return true; +    }); + +    if (!accepted) { +      throw new Error('None of the extension offers can be accepted'); +    } + +    if (opts.serverNoContextTakeover) { +      accepted.server_no_context_takeover = true; +    } +    if (opts.clientNoContextTakeover) { +      accepted.client_no_context_takeover = true; +    } +    if (typeof opts.serverMaxWindowBits === 'number') { +      accepted.server_max_window_bits = opts.serverMaxWindowBits; +    } +    if (typeof opts.clientMaxWindowBits === 'number') { +      accepted.client_max_window_bits = opts.clientMaxWindowBits; +    } else if ( +      accepted.client_max_window_bits === true || +      opts.clientMaxWindowBits === false +    ) { +      delete accepted.client_max_window_bits; +    } + +    return accepted; +  } + +  /** +   * Accept the extension negotiation response. +   * +   * @param {Array} response The extension negotiation response +   * @return {Object} Accepted configuration +   * @private +   */ +  acceptAsClient(response) { +    const params = response[0]; + +    if ( +      this._options.clientNoContextTakeover === false && +      params.client_no_context_takeover +    ) { +      throw new Error('Unexpected parameter "client_no_context_takeover"'); +    } + +    if (!params.client_max_window_bits) { +      if (typeof this._options.clientMaxWindowBits === 'number') { +        params.client_max_window_bits = this._options.clientMaxWindowBits; +      } +    } else if ( +      this._options.clientMaxWindowBits === false || +      (typeof this._options.clientMaxWindowBits === 'number' && +        params.client_max_window_bits > this._options.clientMaxWindowBits) +    ) { +      throw new Error( +        'Unexpected or invalid parameter "client_max_window_bits"' +      ); +    } + +    return params; +  } + +  /** +   * Normalize parameters. +   * +   * @param {Array} configurations The extension negotiation offers/reponse +   * @return {Array} The offers/response with normalized parameters +   * @private +   */ +  normalizeParams(configurations) { +    configurations.forEach((params) => { +      Object.keys(params).forEach((key) => { +        let value = params[key]; + +        if (value.length > 1) { +          throw new Error(`Parameter "${key}" must have only a single value`); +        } + +        value = value[0]; + +        if (key === 'client_max_window_bits') { +          if (value !== true) { +            const num = +value; +            if (!Number.isInteger(num) || num < 8 || num > 15) { +              throw new TypeError( +                `Invalid value for parameter "${key}": ${value}` +              ); +            } +            value = num; +          } else if (!this._isServer) { +            throw new TypeError( +              `Invalid value for parameter "${key}": ${value}` +            ); +          } +        } else if (key === 'server_max_window_bits') { +          const num = +value; +          if (!Number.isInteger(num) || num < 8 || num > 15) { +            throw new TypeError( +              `Invalid value for parameter "${key}": ${value}` +            ); +          } +          value = num; +        } else if ( +          key === 'client_no_context_takeover' || +          key === 'server_no_context_takeover' +        ) { +          if (value !== true) { +            throw new TypeError( +              `Invalid value for parameter "${key}": ${value}` +            ); +          } +        } else { +          throw new Error(`Unknown parameter "${key}"`); +        } + +        params[key] = value; +      }); +    }); + +    return configurations; +  } + +  /** +   * Decompress data. Concurrency limited. +   * +   * @param {Buffer} data Compressed data +   * @param {Boolean} fin Specifies whether or not this is the last fragment +   * @param {Function} callback Callback +   * @public +   */ +  decompress(data, fin, callback) { +    zlibLimiter.add((done) => { +      this._decompress(data, fin, (err, result) => { +        done(); +        callback(err, result); +      }); +    }); +  } + +  /** +   * Compress data. Concurrency limited. +   * +   * @param {Buffer} data Data to compress +   * @param {Boolean} fin Specifies whether or not this is the last fragment +   * @param {Function} callback Callback +   * @public +   */ +  compress(data, fin, callback) { +    zlibLimiter.add((done) => { +      this._compress(data, fin, (err, result) => { +        done(); +        callback(err, result); +      }); +    }); +  } + +  /** +   * Decompress data. +   * +   * @param {Buffer} data Compressed data +   * @param {Boolean} fin Specifies whether or not this is the last fragment +   * @param {Function} callback Callback +   * @private +   */ +  _decompress(data, fin, callback) { +    const endpoint = this._isServer ? 'client' : 'server'; + +    if (!this._inflate) { +      const key = `${endpoint}_max_window_bits`; +      const windowBits = +        typeof this.params[key] !== 'number' +          ? zlib.Z_DEFAULT_WINDOWBITS +          : this.params[key]; + +      this._inflate = zlib.createInflateRaw({ +        ...this._options.zlibInflateOptions, +        windowBits +      }); +      this._inflate[kPerMessageDeflate] = this; +      this._inflate[kTotalLength] = 0; +      this._inflate[kBuffers] = []; +      this._inflate.on('error', inflateOnError); +      this._inflate.on('data', inflateOnData); +    } + +    this._inflate[kCallback] = callback; + +    this._inflate.write(data); +    if (fin) this._inflate.write(TRAILER); + +    this._inflate.flush(() => { +      const err = this._inflate[kError]; + +      if (err) { +        this._inflate.close(); +        this._inflate = null; +        callback(err); +        return; +      } + +      const data = bufferUtil.concat( +        this._inflate[kBuffers], +        this._inflate[kTotalLength] +      ); + +      if (this._inflate._readableState.endEmitted) { +        this._inflate.close(); +        this._inflate = null; +      } else { +        this._inflate[kTotalLength] = 0; +        this._inflate[kBuffers] = []; + +        if (fin && this.params[`${endpoint}_no_context_takeover`]) { +          this._inflate.reset(); +        } +      } + +      callback(null, data); +    }); +  } + +  /** +   * Compress data. +   * +   * @param {Buffer} data Data to compress +   * @param {Boolean} fin Specifies whether or not this is the last fragment +   * @param {Function} callback Callback +   * @private +   */ +  _compress(data, fin, callback) { +    const endpoint = this._isServer ? 'server' : 'client'; + +    if (!this._deflate) { +      const key = `${endpoint}_max_window_bits`; +      const windowBits = +        typeof this.params[key] !== 'number' +          ? zlib.Z_DEFAULT_WINDOWBITS +          : this.params[key]; + +      this._deflate = zlib.createDeflateRaw({ +        ...this._options.zlibDeflateOptions, +        windowBits +      }); + +      this._deflate[kTotalLength] = 0; +      this._deflate[kBuffers] = []; + +      // +      // An `'error'` event is emitted, only on Node.js < 10.0.0, if the +      // `zlib.DeflateRaw` instance is closed while data is being processed. +      // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong +      // time due to an abnormal WebSocket closure. +      // +      this._deflate.on('error', NOOP); +      this._deflate.on('data', deflateOnData); +    } + +    this._deflate[kCallback] = callback; + +    this._deflate.write(data); +    this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { +      if (!this._deflate) { +        // +        // The deflate stream was closed while data was being processed. +        // +        return; +      } + +      let data = bufferUtil.concat( +        this._deflate[kBuffers], +        this._deflate[kTotalLength] +      ); + +      if (fin) data = data.slice(0, data.length - 4); + +      // +      // Ensure that the callback will not be called again in +      // `PerMessageDeflate#cleanup()`. +      // +      this._deflate[kCallback] = null; + +      this._deflate[kTotalLength] = 0; +      this._deflate[kBuffers] = []; + +      if (fin && this.params[`${endpoint}_no_context_takeover`]) { +        this._deflate.reset(); +      } + +      callback(null, data); +    }); +  } +} + +module.exports = PerMessageDeflate; + +/** + * The listener of the `zlib.DeflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function deflateOnData(chunk) { +  this[kBuffers].push(chunk); +  this[kTotalLength] += chunk.length; +} + +/** + * The listener of the `zlib.InflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function inflateOnData(chunk) { +  this[kTotalLength] += chunk.length; + +  if ( +    this[kPerMessageDeflate]._maxPayload < 1 || +    this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload +  ) { +    this[kBuffers].push(chunk); +    return; +  } + +  this[kError] = new RangeError('Max payload size exceeded'); +  this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; +  this[kError][kStatusCode] = 1009; +  this.removeListener('data', inflateOnData); +  this.reset(); +} + +/** + * The listener of the `zlib.InflateRaw` stream `'error'` event. + * + * @param {Error} err The emitted error + * @private + */ +function inflateOnError(err) { +  // +  // There is no need to call `Zlib#close()` as the handle is automatically +  // closed when an error is emitted. +  // +  this[kPerMessageDeflate]._inflate = null; +  err[kStatusCode] = 1007; +  this[kCallback](err); +} diff --git a/node_modules/ws/lib/receiver.js b/node_modules/ws/lib/receiver.js new file mode 100644 index 0000000..1d2af76 --- /dev/null +++ b/node_modules/ws/lib/receiver.js @@ -0,0 +1,607 @@ +'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; +} diff --git a/node_modules/ws/lib/sender.js b/node_modules/ws/lib/sender.js new file mode 100644 index 0000000..441171c --- /dev/null +++ b/node_modules/ws/lib/sender.js @@ -0,0 +1,409 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + +'use strict'; + +const net = require('net'); +const tls = require('tls'); +const { randomFillSync } = require('crypto'); + +const PerMessageDeflate = require('./permessage-deflate'); +const { EMPTY_BUFFER } = require('./constants'); +const { isValidStatusCode } = require('./validation'); +const { mask: applyMask, toBuffer } = require('./buffer-util'); + +const mask = Buffer.alloc(4); + +/** + * HyBi Sender implementation. + */ +class Sender { +  /** +   * Creates a Sender instance. +   * +   * @param {(net.Socket|tls.Socket)} socket The connection socket +   * @param {Object} [extensions] An object containing the negotiated extensions +   */ +  constructor(socket, extensions) { +    this._extensions = extensions || {}; +    this._socket = socket; + +    this._firstFragment = true; +    this._compress = false; + +    this._bufferedBytes = 0; +    this._deflating = false; +    this._queue = []; +  } + +  /** +   * Frames a piece of data according to the HyBi WebSocket protocol. +   * +   * @param {Buffer} data The data to frame +   * @param {Object} options Options object +   * @param {Number} options.opcode The opcode +   * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be +   *     modified +   * @param {Boolean} [options.fin=false] Specifies whether or not to set the +   *     FIN bit +   * @param {Boolean} [options.mask=false] Specifies whether or not to mask +   *     `data` +   * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the +   *     RSV1 bit +   * @return {Buffer[]} The framed data as a list of `Buffer` instances +   * @public +   */ +  static frame(data, options) { +    const merge = options.mask && options.readOnly; +    let offset = options.mask ? 6 : 2; +    let payloadLength = data.length; + +    if (data.length >= 65536) { +      offset += 8; +      payloadLength = 127; +    } else if (data.length > 125) { +      offset += 2; +      payloadLength = 126; +    } + +    const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + +    target[0] = options.fin ? options.opcode | 0x80 : options.opcode; +    if (options.rsv1) target[0] |= 0x40; + +    target[1] = payloadLength; + +    if (payloadLength === 126) { +      target.writeUInt16BE(data.length, 2); +    } else if (payloadLength === 127) { +      target.writeUInt32BE(0, 2); +      target.writeUInt32BE(data.length, 6); +    } + +    if (!options.mask) return [target, data]; + +    randomFillSync(mask, 0, 4); + +    target[1] |= 0x80; +    target[offset - 4] = mask[0]; +    target[offset - 3] = mask[1]; +    target[offset - 2] = mask[2]; +    target[offset - 1] = mask[3]; + +    if (merge) { +      applyMask(data, mask, target, offset, data.length); +      return [target]; +    } + +    applyMask(data, mask, data, 0, data.length); +    return [target, data]; +  } + +  /** +   * Sends a close message to the other peer. +   * +   * @param {Number} [code] The status code component of the body +   * @param {String} [data] The message component of the body +   * @param {Boolean} [mask=false] Specifies whether or not to mask the message +   * @param {Function} [cb] Callback +   * @public +   */ +  close(code, data, mask, cb) { +    let buf; + +    if (code === undefined) { +      buf = EMPTY_BUFFER; +    } else if (typeof code !== 'number' || !isValidStatusCode(code)) { +      throw new TypeError('First argument must be a valid error code number'); +    } else if (data === undefined || data === '') { +      buf = Buffer.allocUnsafe(2); +      buf.writeUInt16BE(code, 0); +    } else { +      const length = Buffer.byteLength(data); + +      if (length > 123) { +        throw new RangeError('The message must not be greater than 123 bytes'); +      } + +      buf = Buffer.allocUnsafe(2 + length); +      buf.writeUInt16BE(code, 0); +      buf.write(data, 2); +    } + +    if (this._deflating) { +      this.enqueue([this.doClose, buf, mask, cb]); +    } else { +      this.doClose(buf, mask, cb); +    } +  } + +  /** +   * Frames and sends a close message. +   * +   * @param {Buffer} data The message to send +   * @param {Boolean} [mask=false] Specifies whether or not to mask `data` +   * @param {Function} [cb] Callback +   * @private +   */ +  doClose(data, mask, cb) { +    this.sendFrame( +      Sender.frame(data, { +        fin: true, +        rsv1: false, +        opcode: 0x08, +        mask, +        readOnly: false +      }), +      cb +    ); +  } + +  /** +   * Sends a ping message to the other peer. +   * +   * @param {*} data The message to send +   * @param {Boolean} [mask=false] Specifies whether or not to mask `data` +   * @param {Function} [cb] Callback +   * @public +   */ +  ping(data, mask, cb) { +    const buf = toBuffer(data); + +    if (buf.length > 125) { +      throw new RangeError('The data size must not be greater than 125 bytes'); +    } + +    if (this._deflating) { +      this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); +    } else { +      this.doPing(buf, mask, toBuffer.readOnly, cb); +    } +  } + +  /** +   * Frames and sends a ping message. +   * +   * @param {Buffer} data The message to send +   * @param {Boolean} [mask=false] Specifies whether or not to mask `data` +   * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified +   * @param {Function} [cb] Callback +   * @private +   */ +  doPing(data, mask, readOnly, cb) { +    this.sendFrame( +      Sender.frame(data, { +        fin: true, +        rsv1: false, +        opcode: 0x09, +        mask, +        readOnly +      }), +      cb +    ); +  } + +  /** +   * Sends a pong message to the other peer. +   * +   * @param {*} data The message to send +   * @param {Boolean} [mask=false] Specifies whether or not to mask `data` +   * @param {Function} [cb] Callback +   * @public +   */ +  pong(data, mask, cb) { +    const buf = toBuffer(data); + +    if (buf.length > 125) { +      throw new RangeError('The data size must not be greater than 125 bytes'); +    } + +    if (this._deflating) { +      this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); +    } else { +      this.doPong(buf, mask, toBuffer.readOnly, cb); +    } +  } + +  /** +   * Frames and sends a pong message. +   * +   * @param {Buffer} data The message to send +   * @param {Boolean} [mask=false] Specifies whether or not to mask `data` +   * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified +   * @param {Function} [cb] Callback +   * @private +   */ +  doPong(data, mask, readOnly, cb) { +    this.sendFrame( +      Sender.frame(data, { +        fin: true, +        rsv1: false, +        opcode: 0x0a, +        mask, +        readOnly +      }), +      cb +    ); +  } + +  /** +   * Sends a data message to the other peer. +   * +   * @param {*} data The message to send +   * @param {Object} options Options object +   * @param {Boolean} [options.compress=false] Specifies whether or not to +   *     compress `data` +   * @param {Boolean} [options.binary=false] Specifies whether `data` is binary +   *     or text +   * @param {Boolean} [options.fin=false] Specifies whether the fragment is the +   *     last one +   * @param {Boolean} [options.mask=false] Specifies whether or not to mask +   *     `data` +   * @param {Function} [cb] Callback +   * @public +   */ +  send(data, options, cb) { +    const buf = toBuffer(data); +    const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; +    let opcode = options.binary ? 2 : 1; +    let rsv1 = options.compress; + +    if (this._firstFragment) { +      this._firstFragment = false; +      if (rsv1 && perMessageDeflate) { +        rsv1 = buf.length >= perMessageDeflate._threshold; +      } +      this._compress = rsv1; +    } else { +      rsv1 = false; +      opcode = 0; +    } + +    if (options.fin) this._firstFragment = true; + +    if (perMessageDeflate) { +      const opts = { +        fin: options.fin, +        rsv1, +        opcode, +        mask: options.mask, +        readOnly: toBuffer.readOnly +      }; + +      if (this._deflating) { +        this.enqueue([this.dispatch, buf, this._compress, opts, cb]); +      } else { +        this.dispatch(buf, this._compress, opts, cb); +      } +    } else { +      this.sendFrame( +        Sender.frame(buf, { +          fin: options.fin, +          rsv1: false, +          opcode, +          mask: options.mask, +          readOnly: toBuffer.readOnly +        }), +        cb +      ); +    } +  } + +  /** +   * Dispatches a data message. +   * +   * @param {Buffer} data The message to send +   * @param {Boolean} [compress=false] Specifies whether or not to compress +   *     `data` +   * @param {Object} options Options object +   * @param {Number} options.opcode The opcode +   * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be +   *     modified +   * @param {Boolean} [options.fin=false] Specifies whether or not to set the +   *     FIN bit +   * @param {Boolean} [options.mask=false] Specifies whether or not to mask +   *     `data` +   * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the +   *     RSV1 bit +   * @param {Function} [cb] Callback +   * @private +   */ +  dispatch(data, compress, options, cb) { +    if (!compress) { +      this.sendFrame(Sender.frame(data, options), cb); +      return; +    } + +    const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + +    this._bufferedBytes += data.length; +    this._deflating = true; +    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 callback = this._queue[i][4]; + +          if (typeof callback === 'function') callback(err); +        } + +        return; +      } + +      this._bufferedBytes -= data.length; +      this._deflating = false; +      options.readOnly = false; +      this.sendFrame(Sender.frame(buf, options), cb); +      this.dequeue(); +    }); +  } + +  /** +   * Executes queued send operations. +   * +   * @private +   */ +  dequeue() { +    while (!this._deflating && this._queue.length) { +      const params = this._queue.shift(); + +      this._bufferedBytes -= params[1].length; +      Reflect.apply(params[0], this, params.slice(1)); +    } +  } + +  /** +   * Enqueues a send operation. +   * +   * @param {Array} params Send operation parameters. +   * @private +   */ +  enqueue(params) { +    this._bufferedBytes += params[1].length; +    this._queue.push(params); +  } + +  /** +   * Sends a frame. +   * +   * @param {Buffer[]} list The frame to send +   * @param {Function} [cb] Callback +   * @private +   */ +  sendFrame(list, cb) { +    if (list.length === 2) { +      this._socket.cork(); +      this._socket.write(list[0]); +      this._socket.write(list[1], cb); +      this._socket.uncork(); +    } else { +      this._socket.write(list[0], cb); +    } +  } +} + +module.exports = Sender; diff --git a/node_modules/ws/lib/stream.js b/node_modules/ws/lib/stream.js new file mode 100644 index 0000000..19e1bff --- /dev/null +++ b/node_modules/ws/lib/stream.js @@ -0,0 +1,180 @@ +'use strict'; + +const { Duplex } = require('stream'); + +/** + * Emits the `'close'` event on a stream. + * + * @param {Duplex} stream The stream. + * @private + */ +function emitClose(stream) { +  stream.emit('close'); +} + +/** + * The listener of the `'end'` event. + * + * @private + */ +function duplexOnEnd() { +  if (!this.destroyed && this._writableState.finished) { +    this.destroy(); +  } +} + +/** + * The listener of the `'error'` event. + * + * @param {Error} err The error + * @private + */ +function duplexOnError(err) { +  this.removeListener('error', duplexOnError); +  this.destroy(); +  if (this.listenerCount('error') === 0) { +    // Do not suppress the throwing behavior. +    this.emit('error', err); +  } +} + +/** + * Wraps a `WebSocket` in a duplex stream. + * + * @param {WebSocket} ws The `WebSocket` to wrap + * @param {Object} [options] The options for the `Duplex` constructor + * @return {Duplex} The duplex stream + * @public + */ +function createWebSocketStream(ws, options) { +  let resumeOnReceiverDrain = true; +  let terminateOnDestroy = true; + +  function receiverOnDrain() { +    if (resumeOnReceiverDrain) ws._socket.resume(); +  } + +  if (ws.readyState === ws.CONNECTING) { +    ws.once('open', function open() { +      ws._receiver.removeAllListeners('drain'); +      ws._receiver.on('drain', receiverOnDrain); +    }); +  } else { +    ws._receiver.removeAllListeners('drain'); +    ws._receiver.on('drain', receiverOnDrain); +  } + +  const duplex = new Duplex({ +    ...options, +    autoDestroy: false, +    emitClose: false, +    objectMode: false, +    writableObjectMode: false +  }); + +  ws.on('message', function message(msg) { +    if (!duplex.push(msg)) { +      resumeOnReceiverDrain = false; +      ws._socket.pause(); +    } +  }); + +  ws.once('error', function error(err) { +    if (duplex.destroyed) return; + +    // Prevent `ws.terminate()` from being called by `duplex._destroy()`. +    // +    // - If the `'error'` event is emitted before the `'open'` event, then +    //   `ws.terminate()` is a noop as no socket is assigned. +    // - Otherwise, the error is re-emitted by the listener of the `'error'` +    //   event of the `Receiver` object. The listener already closes the +    //   connection by calling `ws.close()`. This allows a close frame to be +    //   sent to the other peer. If `ws.terminate()` is called right after this, +    //   then the close frame might not be sent. +    terminateOnDestroy = false; +    duplex.destroy(err); +  }); + +  ws.once('close', function close() { +    if (duplex.destroyed) return; + +    duplex.push(null); +  }); + +  duplex._destroy = function (err, callback) { +    if (ws.readyState === ws.CLOSED) { +      callback(err); +      process.nextTick(emitClose, duplex); +      return; +    } + +    let called = false; + +    ws.once('error', function error(err) { +      called = true; +      callback(err); +    }); + +    ws.once('close', function close() { +      if (!called) callback(err); +      process.nextTick(emitClose, duplex); +    }); + +    if (terminateOnDestroy) ws.terminate(); +  }; + +  duplex._final = function (callback) { +    if (ws.readyState === ws.CONNECTING) { +      ws.once('open', function open() { +        duplex._final(callback); +      }); +      return; +    } + +    // If the value of the `_socket` property is `null` it means that `ws` is a +    // client websocket and the handshake failed. In fact, when this happens, a +    // socket is never assigned to the websocket. Wait for the `'error'` event +    // that will be emitted by the websocket. +    if (ws._socket === null) return; + +    if (ws._socket._writableState.finished) { +      callback(); +      if (duplex._readableState.endEmitted) duplex.destroy(); +    } else { +      ws._socket.once('finish', function finish() { +        // `duplex` is not destroyed here because the `'end'` event will be +        // emitted on `duplex` after this `'finish'` event. The EOF signaling +        // `null` chunk is, in fact, pushed when the websocket emits `'close'`. +        callback(); +      }); +      ws.close(); +    } +  }; + +  duplex._read = function () { +    if ( +      (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) && +      !resumeOnReceiverDrain +    ) { +      resumeOnReceiverDrain = true; +      if (!ws._receiver._writableState.needDrain) ws._socket.resume(); +    } +  }; + +  duplex._write = function (chunk, encoding, callback) { +    if (ws.readyState === ws.CONNECTING) { +      ws.once('open', function open() { +        duplex._write(chunk, encoding, callback); +      }); +      return; +    } + +    ws.send(chunk, callback); +  }; + +  duplex.on('end', duplexOnEnd); +  duplex.on('error', duplexOnError); +  return duplex; +} + +module.exports = createWebSocketStream; diff --git a/node_modules/ws/lib/validation.js b/node_modules/ws/lib/validation.js new file mode 100644 index 0000000..169ac6f --- /dev/null +++ b/node_modules/ws/lib/validation.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +function isValidStatusCode(code) { +  return ( +    (code >= 1000 && +      code <= 1014 && +      code !== 1004 && +      code !== 1005 && +      code !== 1006) || +    (code >= 3000 && code <= 4999) +  ); +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { +  const len = buf.length; +  let i = 0; + +  while (i < len) { +    if ((buf[i] & 0x80) === 0) { +      // 0xxxxxxx +      i++; +    } else if ((buf[i] & 0xe0) === 0xc0) { +      // 110xxxxx 10xxxxxx +      if ( +        i + 1 === len || +        (buf[i + 1] & 0xc0) !== 0x80 || +        (buf[i] & 0xfe) === 0xc0 // Overlong +      ) { +        return false; +      } + +      i += 2; +    } else if ((buf[i] & 0xf0) === 0xe0) { +      // 1110xxxx 10xxxxxx 10xxxxxx +      if ( +        i + 2 >= len || +        (buf[i + 1] & 0xc0) !== 0x80 || +        (buf[i + 2] & 0xc0) !== 0x80 || +        (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong +        (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) +      ) { +        return false; +      } + +      i += 3; +    } else if ((buf[i] & 0xf8) === 0xf0) { +      // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx +      if ( +        i + 3 >= len || +        (buf[i + 1] & 0xc0) !== 0x80 || +        (buf[i + 2] & 0xc0) !== 0x80 || +        (buf[i + 3] & 0xc0) !== 0x80 || +        (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong +        (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || +        buf[i] > 0xf4 // > U+10FFFF +      ) { +        return false; +      } + +      i += 4; +    } else { +      return false; +    } +  } + +  return true; +} + +try { +  let isValidUTF8 = require('utf-8-validate'); + +  /* istanbul ignore if */ +  if (typeof isValidUTF8 === 'object') { +    isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0 +  } + +  module.exports = { +    isValidStatusCode, +    isValidUTF8(buf) { +      return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); +    } +  }; +} catch (e) /* istanbul ignore next */ { +  module.exports = { +    isValidStatusCode, +    isValidUTF8: _isValidUTF8 +  }; +} diff --git a/node_modules/ws/lib/websocket-server.js b/node_modules/ws/lib/websocket-server.js new file mode 100644 index 0000000..fe7fdf5 --- /dev/null +++ b/node_modules/ws/lib/websocket-server.js @@ -0,0 +1,447 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); +const { createHash } = require('crypto'); + +const PerMessageDeflate = require('./permessage-deflate'); +const WebSocket = require('./websocket'); +const { format, parse } = require('./extension'); +const { GUID, kWebSocket } = require('./constants'); + +const keyRegex = /^[+/0-9A-Za-z]{22}==$/; + +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + +/** + * Class representing a WebSocket server. + * + * @extends EventEmitter + */ +class WebSocketServer extends EventEmitter { +  /** +   * Create a `WebSocketServer` instance. +   * +   * @param {Object} options Configuration options +   * @param {Number} [options.backlog=511] The maximum length of the queue of +   *     pending connections +   * @param {Boolean} [options.clientTracking=true] Specifies whether or not to +   *     track clients +   * @param {Function} [options.handleProtocols] A hook to handle protocols +   * @param {String} [options.host] The hostname where to bind the server +   * @param {Number} [options.maxPayload=104857600] The maximum allowed message +   *     size +   * @param {Boolean} [options.noServer=false] Enable no server mode +   * @param {String} [options.path] Accept only connections matching this path +   * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable +   *     permessage-deflate +   * @param {Number} [options.port] The port where to bind the server +   * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S +   *     server to use +   * @param {Function} [options.verifyClient] A hook to reject connections +   * @param {Function} [callback] A listener for the `listening` event +   */ +  constructor(options, callback) { +    super(); + +    options = { +      maxPayload: 100 * 1024 * 1024, +      perMessageDeflate: false, +      handleProtocols: null, +      clientTracking: true, +      verifyClient: null, +      noServer: false, +      backlog: null, // use default (511 as implemented in net.js) +      server: null, +      host: null, +      path: null, +      port: null, +      ...options +    }; + +    if ( +      (options.port == null && !options.server && !options.noServer) || +      (options.port != null && (options.server || options.noServer)) || +      (options.server && options.noServer) +    ) { +      throw new TypeError( +        'One and only one of the "port", "server", or "noServer" options ' + +          'must be specified' +      ); +    } + +    if (options.port != null) { +      this._server = http.createServer((req, res) => { +        const body = http.STATUS_CODES[426]; + +        res.writeHead(426, { +          'Content-Length': body.length, +          'Content-Type': 'text/plain' +        }); +        res.end(body); +      }); +      this._server.listen( +        options.port, +        options.host, +        options.backlog, +        callback +      ); +    } else if (options.server) { +      this._server = options.server; +    } + +    if (this._server) { +      const emitConnection = this.emit.bind(this, 'connection'); + +      this._removeListeners = addListeners(this._server, { +        listening: this.emit.bind(this, 'listening'), +        error: this.emit.bind(this, 'error'), +        upgrade: (req, socket, head) => { +          this.handleUpgrade(req, socket, head, emitConnection); +        } +      }); +    } + +    if (options.perMessageDeflate === true) options.perMessageDeflate = {}; +    if (options.clientTracking) this.clients = new Set(); +    this.options = options; +    this._state = RUNNING; +  } + +  /** +   * Returns the bound address, the address family name, and port of the server +   * as reported by the operating system if listening on an IP socket. +   * If the server is listening on a pipe or UNIX domain socket, the name is +   * returned as a string. +   * +   * @return {(Object|String|null)} The address of the server +   * @public +   */ +  address() { +    if (this.options.noServer) { +      throw new Error('The server is operating in "noServer" mode'); +    } + +    if (!this._server) return null; +    return this._server.address(); +  } + +  /** +   * Close the server. +   * +   * @param {Function} [cb] Callback +   * @public +   */ +  close(cb) { +    if (cb) this.once('close', cb); + +    if (this._state === CLOSED) { +      process.nextTick(emitClose, this); +      return; +    } + +    if (this._state === CLOSING) return; +    this._state = CLOSING; + +    // +    // Terminate all associated clients. +    // +    if (this.clients) { +      for (const client of this.clients) client.terminate(); +    } + +    const server = this._server; + +    if (server) { +      this._removeListeners(); +      this._removeListeners = this._server = null; + +      // +      // Close the http server if it was internally created. +      // +      if (this.options.port != null) { +        server.close(emitClose.bind(undefined, this)); +        return; +      } +    } + +    process.nextTick(emitClose, this); +  } + +  /** +   * See if a given request should be handled by this server instance. +   * +   * @param {http.IncomingMessage} req Request object to inspect +   * @return {Boolean} `true` if the request is valid, else `false` +   * @public +   */ +  shouldHandle(req) { +    if (this.options.path) { +      const index = req.url.indexOf('?'); +      const pathname = index !== -1 ? req.url.slice(0, index) : req.url; + +      if (pathname !== this.options.path) return false; +    } + +    return true; +  } + +  /** +   * 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 {Buffer} head The first packet of the upgraded stream +   * @param {Function} cb Callback +   * @public +   */ +  handleUpgrade(req, socket, head, cb) { +    socket.on('error', socketOnError); + +    const key = +      req.headers['sec-websocket-key'] !== undefined +        ? req.headers['sec-websocket-key'].trim() +        : false; +    const version = +req.headers['sec-websocket-version']; +    const extensions = {}; + +    if ( +      req.method !== 'GET' || +      req.headers.upgrade.toLowerCase() !== 'websocket' || +      !key || +      !keyRegex.test(key) || +      (version !== 8 && version !== 13) || +      !this.shouldHandle(req) +    ) { +      return abortHandshake(socket, 400); +    } + +    if (this.options.perMessageDeflate) { +      const perMessageDeflate = new PerMessageDeflate( +        this.options.perMessageDeflate, +        true, +        this.options.maxPayload +      ); + +      try { +        const offers = parse(req.headers['sec-websocket-extensions']); + +        if (offers[PerMessageDeflate.extensionName]) { +          perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); +          extensions[PerMessageDeflate.extensionName] = perMessageDeflate; +        } +      } catch (err) { +        return abortHandshake(socket, 400); +      } +    } + +    // +    // Optionally call external client verification handler. +    // +    if (this.options.verifyClient) { +      const info = { +        origin: +          req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], +        secure: !!(req.socket.authorized || req.socket.encrypted), +        req +      }; + +      if (this.options.verifyClient.length === 2) { +        this.options.verifyClient(info, (verified, code, message, headers) => { +          if (!verified) { +            return abortHandshake(socket, code || 401, message, headers); +          } + +          this.completeUpgrade(key, extensions, req, socket, head, cb); +        }); +        return; +      } + +      if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); +    } + +    this.completeUpgrade(key, extensions, req, socket, head, cb); +  } + +  /** +   * Upgrade the connection to WebSocket. +   * +   * @param {String} key The value of the `Sec-WebSocket-Key` header +   * @param {Object} extensions The accepted extensions +   * @param {http.IncomingMessage} req The request object +   * @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 {Function} cb Callback +   * @throws {Error} If called more than once with the same socket +   * @private +   */ +  completeUpgrade(key, extensions, req, socket, head, cb) { +    // +    // Destroy the socket if the client has already sent a FIN packet. +    // +    if (!socket.readable || !socket.writable) return socket.destroy(); + +    if (socket[kWebSocket]) { +      throw new Error( +        'server.handleUpgrade() was called more than once with the same ' + +          'socket, possibly due to a misconfiguration' +      ); +    } + +    if (this._state > RUNNING) return abortHandshake(socket, 503); + +    const digest = createHash('sha1') +      .update(key + GUID) +      .digest('base64'); + +    const headers = [ +      'HTTP/1.1 101 Switching Protocols', +      'Upgrade: websocket', +      'Connection: Upgrade', +      `Sec-WebSocket-Accept: ${digest}` +    ]; + +    const ws = new WebSocket(null); +    let protocol = req.headers['sec-websocket-protocol']; + +    if (protocol) { +      protocol = protocol.split(',').map(trim); + +      // +      // Optionally call external protocol selection handler. +      // +      if (this.options.handleProtocols) { +        protocol = this.options.handleProtocols(protocol, req); +      } else { +        protocol = protocol[0]; +      } + +      if (protocol) { +        headers.push(`Sec-WebSocket-Protocol: ${protocol}`); +        ws._protocol = protocol; +      } +    } + +    if (extensions[PerMessageDeflate.extensionName]) { +      const params = extensions[PerMessageDeflate.extensionName].params; +      const value = format({ +        [PerMessageDeflate.extensionName]: [params] +      }); +      headers.push(`Sec-WebSocket-Extensions: ${value}`); +      ws._extensions = extensions; +    } + +    // +    // Allow external modification/inspection of handshake headers. +    // +    this.emit('headers', headers, req); + +    socket.write(headers.concat('\r\n').join('\r\n')); +    socket.removeListener('error', socketOnError); + +    ws.setSocket(socket, head, this.options.maxPayload); + +    if (this.clients) { +      this.clients.add(ws); +      ws.on('close', () => this.clients.delete(ws)); +    } + +    cb(ws, req); +  } +} + +module.exports = WebSocketServer; + +/** + * Add event listeners on an `EventEmitter` using a map of <event, listener> + * pairs. + * + * @param {EventEmitter} server The event emitter + * @param {Object.<String, Function>} map The listeners to add + * @return {Function} A function that will remove the added listeners when + *     called + * @private + */ +function addListeners(server, map) { +  for (const event of Object.keys(map)) server.on(event, map[event]); + +  return function removeListeners() { +    for (const event of Object.keys(map)) { +      server.removeListener(event, map[event]); +    } +  }; +} + +/** + * Emit a `'close'` event on an `EventEmitter`. + * + * @param {EventEmitter} server The event emitter + * @private + */ +function emitClose(server) { +  server._state = CLOSED; +  server.emit('close'); +} + +/** + * Handle premature socket errors. + * + * @private + */ +function socketOnError() { +  this.destroy(); +} + +/** + * Close the connection when preconditions are not fulfilled. + * + * @param {(net.Socket|tls.Socket)} 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 + * @private + */ +function abortHandshake(socket, code, message, headers) { +  if (socket.writable) { +    message = message || http.STATUS_CODES[code]; +    headers = { +      Connection: 'close', +      'Content-Type': 'text/html', +      'Content-Length': Buffer.byteLength(message), +      ...headers +    }; + +    socket.write( +      `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + +        Object.keys(headers) +          .map((h) => `${h}: ${headers[h]}`) +          .join('\r\n') + +        '\r\n\r\n' + +        message +    ); +  } + +  socket.removeListener('error', socketOnError); +  socket.destroy(); +} + +/** + * Remove whitespace characters from both ends of a string. + * + * @param {String} str The string + * @return {String} A new string representing `str` stripped of whitespace + *     characters from both its beginning and end + * @private + */ +function trim(str) { +  return str.trim(); +} diff --git a/node_modules/ws/lib/websocket.js b/node_modules/ws/lib/websocket.js new file mode 100644 index 0000000..c39b34c --- /dev/null +++ b/node_modules/ws/lib/websocket.js @@ -0,0 +1,1174 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const https = require('https'); +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'); +const Receiver = require('./receiver'); +const Sender = require('./sender'); +const { +  BINARY_TYPES, +  EMPTY_BUFFER, +  GUID, +  kStatusCode, +  kWebSocket, +  NOOP +} = require('./constants'); +const { addEventListener, removeEventListener } = require('./event-target'); +const { format, parse } = require('./extension'); +const { toBuffer } = require('./buffer-util'); + +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const protocolVersions = [8, 13]; +const closeTimeout = 30 * 1000; + +/** + * Class representing a WebSocket. + * + * @extends EventEmitter + */ +class WebSocket extends EventEmitter { +  /** +   * Create a new `WebSocket`. +   * +   * @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._binaryType = BINARY_TYPES[0]; +    this._closeCode = 1006; +    this._closeFrameReceived = false; +    this._closeFrameSent = false; +    this._closeMessage = ''; +    this._closeTimer = null; +    this._extensions = {}; +    this._protocol = ''; +    this._readyState = WebSocket.CONNECTING; +    this._receiver = null; +    this._sender = null; +    this._socket = null; + +    if (address !== null) { +      this._bufferedAmount = 0; +      this._isServer = false; +      this._redirects = 0; + +      if (Array.isArray(protocols)) { +        protocols = protocols.join(', '); +      } else if (typeof protocols === 'object' && protocols !== null) { +        options = protocols; +        protocols = undefined; +      } + +      initAsClient(this, address, protocols, options); +    } else { +      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). +   * +   * @type {String} +   */ +  get binaryType() { +    return this._binaryType; +  } + +  set binaryType(type) { +    if (!BINARY_TYPES.includes(type)) return; + +    this._binaryType = type; + +    // +    // Allow to change `binaryType` on the fly. +    // +    if (this._receiver) this._receiver._binaryType = type; +  } + +  /** +   * @type {Number} +   */ +  get bufferedAmount() { +    if (!this._socket) return this._bufferedAmount; + +    return this._socket._writableState.length + this._sender._bufferedBytes; +  } + +  /** +   * @type {String} +   */ +  get extensions() { +    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|tls.Socket)} socket The network socket between the +   *     server and client +   * @param {Buffer} head The first packet of the upgraded stream +   * @param {Number} [maxPayload=0] The maximum allowed message size +   * @private +   */ +  setSocket(socket, head, maxPayload) { +    const receiver = new Receiver( +      this.binaryType, +      this._extensions, +      this._isServer, +      maxPayload +    ); + +    this._sender = new Sender(socket, this._extensions); +    this._receiver = receiver; +    this._socket = socket; + +    receiver[kWebSocket] = this; +    socket[kWebSocket] = this; + +    receiver.on('conclude', receiverOnConclude); +    receiver.on('drain', receiverOnDrain); +    receiver.on('error', receiverOnError); +    receiver.on('message', receiverOnMessage); +    receiver.on('ping', receiverOnPing); +    receiver.on('pong', receiverOnPong); + +    socket.setTimeout(0); +    socket.setNoDelay(); + +    if (head.length > 0) socket.unshift(head); + +    socket.on('close', socketOnClose); +    socket.on('data', socketOnData); +    socket.on('end', socketOnEnd); +    socket.on('error', socketOnError); + +    this._readyState = WebSocket.OPEN; +    this.emit('open'); +  } + +  /** +   * Emit the `'close'` event. +   * +   * @private +   */ +  emitClose() { +    if (!this._socket) { +      this._readyState = WebSocket.CLOSED; +      this.emit('close', this._closeCode, this._closeMessage); +      return; +    } + +    if (this._extensions[PerMessageDeflate.extensionName]) { +      this._extensions[PerMessageDeflate.extensionName].cleanup(); +    } + +    this._receiver.removeAllListeners(); +    this._readyState = WebSocket.CLOSED; +    this.emit('close', this._closeCode, this._closeMessage); +  } + +  /** +   * Start a closing handshake. +   * +   *          +----------+   +-----------+   +----------+ +   *     - - -|ws.close()|-->|close frame|-->|ws.close()|- - - +   *    |     +----------+   +-----------+   +----------+     | +   *          +----------+   +-----------+         | +   * CLOSING  |ws.close()|<--|close frame|<--+-----+       CLOSING +   *          +----------+   +-----------+   | +   *    |           |                        |   +---+        | +   *                +------------------------+-->|fin| - - - - +   *    |         +---+                      |   +---+ +   *     - - - - -|fin|<---------------------+ +   *              +---+ +   * +   * @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) { +    if (this.readyState === WebSocket.CLOSED) return; +    if (this.readyState === WebSocket.CONNECTING) { +      const msg = 'WebSocket was closed before the connection was established'; +      return abortHandshake(this, this._req, msg); +    } + +    if (this.readyState === WebSocket.CLOSING) { +      if ( +        this._closeFrameSent && +        (this._closeFrameReceived || this._receiver._writableState.errorEmitted) +      ) { +        this._socket.end(); +      } + +      return; +    } + +    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 +      // want to know if the close frame has been sent here. +      // +      if (err) return; + +      this._closeFrameSent = true; + +      if ( +        this._closeFrameReceived || +        this._receiver._writableState.errorEmitted +      ) { +        this._socket.end(); +      } +    }); + +    // +    // Specify a timeout for the closing handshake to complete. +    // +    this._closeTimer = setTimeout( +      this._socket.destroy.bind(this._socket), +      closeTimeout +    ); +  } + +  /** +   * 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 +   * @public +   */ +  ping(data, mask, cb) { +    if (this.readyState === WebSocket.CONNECTING) { +      throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); +    } + +    if (typeof data === 'function') { +      cb = data; +      data = mask = undefined; +    } else if (typeof mask === 'function') { +      cb = mask; +      mask = undefined; +    } + +    if (typeof data === 'number') data = data.toString(); + +    if (this.readyState !== WebSocket.OPEN) { +      sendAfterClose(this, data, cb); +      return; +    } + +    if (mask === undefined) mask = !this._isServer; +    this._sender.ping(data || EMPTY_BUFFER, mask, cb); +  } + +  /** +   * 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 +   * @public +   */ +  pong(data, mask, cb) { +    if (this.readyState === WebSocket.CONNECTING) { +      throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); +    } + +    if (typeof data === 'function') { +      cb = data; +      data = mask = undefined; +    } else if (typeof mask === 'function') { +      cb = mask; +      mask = undefined; +    } + +    if (typeof data === 'number') data = data.toString(); + +    if (this.readyState !== WebSocket.OPEN) { +      sendAfterClose(this, data, cb); +      return; +    } + +    if (mask === undefined) mask = !this._isServer; +    this._sender.pong(data || EMPTY_BUFFER, mask, cb); +  } + +  /** +   * 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 +   *     `data` +   * @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) { +    if (this.readyState === WebSocket.CONNECTING) { +      throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); +    } + +    if (typeof options === 'function') { +      cb = options; +      options = {}; +    } + +    if (typeof data === 'number') data = data.toString(); + +    if (this.readyState !== WebSocket.OPEN) { +      sendAfterClose(this, data, cb); +      return; +    } + +    const opts = { +      binary: typeof data !== 'string', +      mask: !this._isServer, +      compress: true, +      fin: true, +      ...options +    }; + +    if (!this._extensions[PerMessageDeflate.extensionName]) { +      opts.compress = false; +    } + +    this._sender.send(data || EMPTY_BUFFER, opts, cb); +  } + +  /** +   * Forcibly close the connection. +   * +   * @public +   */ +  terminate() { +    if (this.readyState === WebSocket.CLOSED) return; +    if (this.readyState === WebSocket.CONNECTING) { +      const msg = 'WebSocket was closed before the connection was established'; +      return abortHandshake(this, this._req, msg); +    } + +    if (this._socket) { +      this._readyState = WebSocket.CLOSING; +      this._socket.destroy(); +    } +  } +} + +/** + * @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 }); +}); + +// +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface +// +['open', 'error', 'close', 'message'].forEach((method) => { +  Object.defineProperty(WebSocket.prototype, `on${method}`, { +    enumerable: true, +    get() { +      const listeners = this.listeners(method); +      for (let i = 0; i < listeners.length; i++) { +        if (listeners[i]._listener) return listeners[i]._listener; +      } + +      return undefined; +    }, +    set(listener) { +      const listeners = this.listeners(method); +      for (let i = 0; i < listeners.length; i++) { +        // +        // Remove only the listeners added via `addEventListener`. +        // +        if (listeners[i]._listener) this.removeListener(method, listeners[i]); +      } +      this.addEventListener(method, listener); +    } +  }); +}); + +WebSocket.prototype.addEventListener = addEventListener; +WebSocket.prototype.removeEventListener = removeEventListener; + +module.exports = WebSocket; + +/** + * Initialize a WebSocket client. + * + * @param {WebSocket} websocket The client to initialize + * @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 + *     handshake request + * @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=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) { +  const opts = { +    protocolVersion: protocolVersions[1], +    maxPayload: 100 * 1024 * 1024, +    perMessageDeflate: true, +    followRedirects: false, +    maxRedirects: 10, +    ...options, +    createConnection: undefined, +    socketPath: undefined, +    hostname: undefined, +    protocol: undefined, +    timeout: undefined, +    method: undefined, +    host: undefined, +    path: undefined, +    port: undefined +  }; + +  if (!protocolVersions.includes(opts.protocolVersion)) { +    throw new RangeError( +      `Unsupported protocol version: ${opts.protocolVersion} ` + +        `(supported versions: ${protocolVersions.join(', ')})` +    ); +  } + +  let parsedUrl; + +  if (address instanceof URL) { +    parsedUrl = address; +    websocket._url = address.href; +  } else { +    parsedUrl = new URL(address); +    websocket._url = address; +  } + +  const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + +  if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { +    const err = new Error(`Invalid URL: ${websocket.url}`); + +    if (websocket._redirects === 0) { +      throw err; +    } else { +      emitErrorAndClose(websocket, err); +      return; +    } +  } + +  const isSecure = +    parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; +  const defaultPort = isSecure ? 443 : 80; +  const key = randomBytes(16).toString('base64'); +  const get = isSecure ? https.get : http.get; +  let perMessageDeflate; + +  opts.createConnection = isSecure ? tlsConnect : netConnect; +  opts.defaultPort = opts.defaultPort || defaultPort; +  opts.port = parsedUrl.port || defaultPort; +  opts.host = parsedUrl.hostname.startsWith('[') +    ? parsedUrl.hostname.slice(1, -1) +    : parsedUrl.hostname; +  opts.headers = { +    'Sec-WebSocket-Version': opts.protocolVersion, +    'Sec-WebSocket-Key': key, +    Connection: 'Upgrade', +    Upgrade: 'websocket', +    ...opts.headers +  }; +  opts.path = parsedUrl.pathname + parsedUrl.search; +  opts.timeout = opts.handshakeTimeout; + +  if (opts.perMessageDeflate) { +    perMessageDeflate = new PerMessageDeflate( +      opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, +      false, +      opts.maxPayload +    ); +    opts.headers['Sec-WebSocket-Extensions'] = format({ +      [PerMessageDeflate.extensionName]: perMessageDeflate.offer() +    }); +  } +  if (protocols) { +    opts.headers['Sec-WebSocket-Protocol'] = protocols; +  } +  if (opts.origin) { +    if (opts.protocolVersion < 13) { +      opts.headers['Sec-WebSocket-Origin'] = opts.origin; +    } else { +      opts.headers.Origin = opts.origin; +    } +  } +  if (parsedUrl.username || parsedUrl.password) { +    opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; +  } + +  if (isUnixSocket) { +    const parts = opts.path.split(':'); + +    opts.socketPath = parts[0]; +    opts.path = parts[1]; +  } + +  if (opts.followRedirects) { +    if (websocket._redirects === 0) { +      websocket._originalHost = parsedUrl.host; + +      const headers = options && options.headers; + +      // +      // Shallow copy the user provided options so that headers can be changed +      // without mutating the original object. +      // +      options = { ...options, headers: {} }; + +      if (headers) { +        for (const [key, value] of Object.entries(headers)) { +          options.headers[key.toLowerCase()] = value; +        } +      } +    } else if (parsedUrl.host !== websocket._originalHost) { +      // +      // Match curl 7.77.0 behavior and drop the following headers. These +      // headers are also dropped when following a redirect to a subdomain. +      // +      delete opts.headers.authorization; +      delete opts.headers.cookie; +      delete opts.headers.host; +      opts.auth = undefined; +    } + +    // +    // Match curl 7.77.0 behavior and make the first `Authorization` header win. +    // If the `Authorization` header is set, then there is nothing to do as it +    // will take precedence. +    // +    if (opts.auth && !options.headers.authorization) { +      options.headers.authorization = +        'Basic ' + Buffer.from(opts.auth).toString('base64'); +    } +  } + +  let req = (websocket._req = get(opts)); + +  if (opts.timeout) { +    req.on('timeout', () => { +      abortHandshake(websocket, req, 'Opening handshake has timed out'); +    }); +  } + +  req.on('error', (err) => { +    if (req === null || req.aborted) return; + +    req = websocket._req = null; +    emitErrorAndClose(websocket, err); +  }); + +  req.on('response', (res) => { +    const location = res.headers.location; +    const statusCode = res.statusCode; + +    if ( +      location && +      opts.followRedirects && +      statusCode >= 300 && +      statusCode < 400 +    ) { +      if (++websocket._redirects > opts.maxRedirects) { +        abortHandshake(websocket, req, 'Maximum redirects exceeded'); +        return; +      } + +      req.abort(); + +      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)) { +      abortHandshake( +        websocket, +        req, +        `Unexpected server response: ${res.statusCode}` +      ); +    } +  }); + +  req.on('upgrade', (res, socket, head) => { +    websocket.emit('upgrade', res); + +    // +    // The user may have closed the connection from a listener of the `upgrade` +    // event. +    // +    if (websocket.readyState !== WebSocket.CONNECTING) return; + +    req = websocket._req = null; + +    const digest = createHash('sha1') +      .update(key + GUID) +      .digest('base64'); + +    if (res.headers['sec-websocket-accept'] !== digest) { +      abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header'); +      return; +    } + +    const serverProt = res.headers['sec-websocket-protocol']; +    const protList = (protocols || '').split(/, */); +    let protError; + +    if (!protocols && serverProt) { +      protError = 'Server sent a subprotocol but none was requested'; +    } else if (protocols && !serverProt) { +      protError = 'Server sent no subprotocol'; +    } else if (serverProt && !protList.includes(serverProt)) { +      protError = 'Server sent an invalid subprotocol'; +    } + +    if (protError) { +      abortHandshake(websocket, socket, protError); +      return; +    } + +    if (serverProt) websocket._protocol = serverProt; + +    const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + +    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. + * + * @param {Object} options Connection options + * @return {net.Socket} The newly created socket used to start the connection + * @private + */ +function netConnect(options) { +  options.path = options.socketPath; +  return net.connect(options); +} + +/** + * Create a `tls.TLSSocket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {tls.TLSSocket} The newly created socket used to start the connection + * @private + */ +function tlsConnect(options) { +  options.path = undefined; + +  if (!options.servername && options.servername !== '') { +    options.servername = net.isIP(options.host) ? '' : options.host; +  } + +  return tls.connect(options); +} + +/** + * Abort the handshake and emit an error. + * + * @param {WebSocket} websocket The WebSocket instance + * @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; + +  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 { +    stream.destroy(err); +    stream.once('error', websocket.emit.bind(websocket, 'error')); +    stream.once('close', websocket.emitClose.bind(websocket)); +  } +} + +/** + * Handle cases where the `ping()`, `pong()`, or `send()` methods are called + * when the `readyState` attribute is `CLOSING` or `CLOSED`. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {*} [data] The data to send + * @param {Function} [cb] Callback + * @private + */ +function sendAfterClose(websocket, data, cb) { +  if (data) { +    const length = toBuffer(data).length; + +    // +    // The `_bufferedAmount` property is used only when the peer is a client and +    // the opening handshake fails. Under these circumstances, in fact, the +    // `setSocket()` method is not called, so the `_socket` and `_sender` +    // properties are set to `null`. +    // +    if (websocket._socket) websocket._sender._bufferedBytes += length; +    else websocket._bufferedAmount += length; +  } + +  if (cb) { +    const err = new Error( +      `WebSocket is not open: readyState ${websocket.readyState} ` + +        `(${readyStates[websocket.readyState]})` +    ); +    cb(err); +  } +} + +/** + * The listener of the `Receiver` `'conclude'` event. + * + * @param {Number} code The status code + * @param {String} reason The reason for closing + * @private + */ +function receiverOnConclude(code, reason) { +  const websocket = this[kWebSocket]; + +  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); +} + +/** + * The listener of the `Receiver` `'drain'` event. + * + * @private + */ +function receiverOnDrain() { +  this[kWebSocket]._socket.resume(); +} + +/** + * The listener of the `Receiver` `'error'` event. + * + * @param {(RangeError|Error)} err The emitted error + * @private + */ +function receiverOnError(err) { +  const websocket = this[kWebSocket]; + +  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.emit('error', err); +} + +/** + * The listener of the `Receiver` `'finish'` event. + * + * @private + */ +function receiverOnFinish() { +  this[kWebSocket].emitClose(); +} + +/** + * The listener of the `Receiver` `'message'` event. + * + * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @private + */ +function receiverOnMessage(data) { +  this[kWebSocket].emit('message', data); +} + +/** + * The listener of the `Receiver` `'ping'` event. + * + * @param {Buffer} data The data included in the ping frame + * @private + */ +function receiverOnPing(data) { +  const websocket = this[kWebSocket]; + +  websocket.pong(data, !websocket._isServer, NOOP); +  websocket.emit('ping', data); +} + +/** + * The listener of the `Receiver` `'pong'` event. + * + * @param {Buffer} data The data included in the pong frame + * @private + */ +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. + * + * @private + */ +function socketOnClose() { +  const websocket = this[kWebSocket]; + +  this.removeListener('close', socketOnClose); +  this.removeListener('data', socketOnData); +  this.removeListener('end', socketOnEnd); + +  websocket._readyState = WebSocket.CLOSING; + +  let chunk; + +  // +  // The close frame might not have been received or the `'end'` event emitted, +  // for example, if the socket was destroyed due to an error. Ensure that the +  // `receiver` stream is closed after writing any remaining buffered data to +  // 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. +  // +  if ( +    !this._readableState.endEmitted && +    !websocket._closeFrameReceived && +    !websocket._receiver._writableState.errorEmitted && +    (chunk = websocket._socket.read()) !== null +  ) { +    websocket._receiver.write(chunk); +  } + +  websocket._receiver.end(); + +  this[kWebSocket] = undefined; + +  clearTimeout(websocket._closeTimer); + +  if ( +    websocket._receiver._writableState.finished || +    websocket._receiver._writableState.errorEmitted +  ) { +    websocket.emitClose(); +  } else { +    websocket._receiver.on('error', receiverOnFinish); +    websocket._receiver.on('finish', receiverOnFinish); +  } +} + +/** + * The listener of the `net.Socket` `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function socketOnData(chunk) { +  if (!this[kWebSocket]._receiver.write(chunk)) { +    this.pause(); +  } +} + +/** + * The listener of the `net.Socket` `'end'` event. + * + * @private + */ +function socketOnEnd() { +  const websocket = this[kWebSocket]; + +  websocket._readyState = WebSocket.CLOSING; +  websocket._receiver.end(); +  this.end(); +} + +/** + * The listener of the `net.Socket` `'error'` event. + * + * @private + */ +function socketOnError() { +  const websocket = this[kWebSocket]; + +  this.removeListener('error', socketOnError); +  this.on('error', NOOP); + +  if (websocket) { +    websocket._readyState = WebSocket.CLOSING; +    this.destroy(); +  } +}  | 
