aboutsummaryrefslogtreecommitdiffhomepage
path: root/node_modules/ws/lib
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/ws/lib')
-rw-r--r--node_modules/ws/lib/BufferUtil.js71
-rw-r--r--node_modules/ws/lib/Constants.js10
-rw-r--r--node_modules/ws/lib/ErrorCodes.js28
-rw-r--r--node_modules/ws/lib/EventTarget.js155
-rw-r--r--node_modules/ws/lib/Extensions.js67
-rw-r--r--node_modules/ws/lib/PerMessageDeflate.js384
-rw-r--r--node_modules/ws/lib/Receiver.js555
-rw-r--r--node_modules/ws/lib/Sender.js403
-rw-r--r--node_modules/ws/lib/Validation.js17
-rw-r--r--node_modules/ws/lib/WebSocket.js712
-rw-r--r--node_modules/ws/lib/WebSocketServer.js336
11 files changed, 2738 insertions, 0 deletions
diff --git a/node_modules/ws/lib/BufferUtil.js b/node_modules/ws/lib/BufferUtil.js
new file mode 100644
index 0000000..6a35e8f
--- /dev/null
+++ b/node_modules/ws/lib/BufferUtil.js
@@ -0,0 +1,71 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+
+const Buffer = safeBuffer.Buffer;
+
+/**
+ * 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
+ */
+const concat = (list, totalLength) => {
+ const target = Buffer.allocUnsafe(totalLength);
+ var offset = 0;
+
+ for (var i = 0; i < list.length; i++) {
+ const buf = list[i];
+ buf.copy(target, offset);
+ offset += buf.length;
+ }
+
+ return target;
+};
+
+try {
+ const bufferUtil = require('bufferutil');
+
+ module.exports = Object.assign({ concat }, bufferUtil.BufferUtil || bufferUtil);
+} catch (e) /* istanbul ignore next */ {
+ /**
+ * 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
+ */
+ const mask = (source, mask, output, offset, length) => {
+ for (var 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
+ */
+ const unmask = (buffer, mask) => {
+ // Required until https://github.com/nodejs/node/issues/9006 is resolved.
+ const length = buffer.length;
+ for (var i = 0; i < length; i++) {
+ buffer[i] ^= mask[i & 3];
+ }
+ };
+
+ module.exports = { concat, mask, unmask };
+}
diff --git a/node_modules/ws/lib/Constants.js b/node_modules/ws/lib/Constants.js
new file mode 100644
index 0000000..3904414
--- /dev/null
+++ b/node_modules/ws/lib/Constants.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+
+const Buffer = safeBuffer.Buffer;
+
+exports.BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'];
+exports.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
+exports.EMPTY_BUFFER = Buffer.alloc(0);
+exports.NOOP = () => {};
diff --git a/node_modules/ws/lib/ErrorCodes.js b/node_modules/ws/lib/ErrorCodes.js
new file mode 100644
index 0000000..f515571
--- /dev/null
+++ b/node_modules/ws/lib/ErrorCodes.js
@@ -0,0 +1,28 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+module.exports = {
+ isValidErrorCode: function (code) {
+ return (code >= 1000 && code <= 1013 && code !== 1004 && code !== 1005 && code !== 1006) ||
+ (code >= 3000 && code <= 4999);
+ },
+ 1000: 'normal',
+ 1001: 'going away',
+ 1002: 'protocol error',
+ 1003: 'unsupported data',
+ 1004: 'reserved',
+ 1005: 'reserved for extensions',
+ 1006: 'reserved for extensions',
+ 1007: 'inconsistent or invalid data',
+ 1008: 'policy violation',
+ 1009: 'message too big',
+ 1010: 'extension handshake missing',
+ 1011: 'an unexpected condition prevented the request from being fulfilled',
+ 1012: 'service restart',
+ 1013: 'try again later'
+};
diff --git a/node_modules/ws/lib/EventTarget.js b/node_modules/ws/lib/EventTarget.js
new file mode 100644
index 0000000..e30b1b3
--- /dev/null
+++ b/node_modules/ws/lib/EventTarget.js
@@ -0,0 +1,155 @@
+'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 {Boolean} isBinary Specifies if `data` is binary
+ * @param {WebSocket} target A reference to the target to which the event was dispatched
+ */
+ constructor (data, isBinary, target) {
+ super('message', target);
+
+ this.binary = isBinary; // non-standard.
+ 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 = code === undefined || code === 1000;
+ this.reason = reason;
+ this.target = target;
+ this.type = 'close';
+ 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);
+ }
+}
+
+/**
+ * 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} method A string representing the event type to listen for
+ * @param {Function} listener The listener to add
+ * @public
+ */
+ addEventListener (method, listener) {
+ if (typeof listener !== 'function') return;
+
+ function onMessage (data, flags) {
+ listener.call(this, new MessageEvent(data, !!flags.binary, this));
+ }
+
+ function onClose (code, message) {
+ listener.call(this, new CloseEvent(code, message, this));
+ }
+
+ function onError (event) {
+ event.type = 'error';
+ event.target = this;
+ listener.call(this, event);
+ }
+
+ function onOpen () {
+ listener.call(this, new OpenEvent(this));
+ }
+
+ if (method === 'message') {
+ onMessage._listener = listener;
+ this.on(method, onMessage);
+ } else if (method === 'close') {
+ onClose._listener = listener;
+ this.on(method, onClose);
+ } else if (method === 'error') {
+ onError._listener = listener;
+ this.on(method, onError);
+ } else if (method === 'open') {
+ onOpen._listener = listener;
+ this.on(method, onOpen);
+ } else {
+ this.on(method, listener);
+ }
+ },
+
+ /**
+ * Remove an event listener.
+ *
+ * @param {String} method A string representing the event type to remove
+ * @param {Function} listener The listener to remove
+ * @public
+ */
+ removeEventListener (method, listener) {
+ const listeners = this.listeners(method);
+
+ for (var i = 0; i < listeners.length; i++) {
+ if (listeners[i] === listener || listeners[i]._listener === listener) {
+ this.removeListener(method, listeners[i]);
+ }
+ }
+ }
+};
+
+module.exports = EventTarget;
diff --git a/node_modules/ws/lib/Extensions.js b/node_modules/ws/lib/Extensions.js
new file mode 100644
index 0000000..a91910e
--- /dev/null
+++ b/node_modules/ws/lib/Extensions.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Parse the `Sec-WebSocket-Extensions` header into an object.
+ *
+ * @param {String} value field value of the header
+ * @return {Object} The parsed object
+ * @public
+ */
+const parse = (value) => {
+ value = value || '';
+
+ const extensions = {};
+
+ value.split(',').forEach((v) => {
+ const params = v.split(';');
+ const token = params.shift().trim();
+ const paramsList = extensions[token] = extensions[token] || [];
+ const parsedParams = {};
+
+ params.forEach((param) => {
+ const parts = param.trim().split('=');
+ const key = parts[0];
+ var value = parts[1];
+
+ if (value === undefined) {
+ value = true;
+ } else {
+ // unquote value
+ if (value[0] === '"') {
+ value = value.slice(1);
+ }
+ if (value[value.length - 1] === '"') {
+ value = value.slice(0, value.length - 1);
+ }
+ }
+ (parsedParams[key] = parsedParams[key] || []).push(value);
+ });
+
+ paramsList.push(parsedParams);
+ });
+
+ return extensions;
+};
+
+/**
+ * Serialize a parsed `Sec-WebSocket-Extensions` header to a string.
+ *
+ * @param {Object} value The object to format
+ * @return {String} A string representing the given value
+ * @public
+ */
+const format = (value) => {
+ return Object.keys(value).map((token) => {
+ var paramsList = value[token];
+ if (!Array.isArray(paramsList)) paramsList = [paramsList];
+ return paramsList.map((params) => {
+ return [token].concat(Object.keys(params).map((k) => {
+ var p = params[k];
+ if (!Array.isArray(p)) p = [p];
+ return p.map((v) => v === true ? k : `${k}=${v}`).join('; ');
+ })).join('; ');
+ }).join(', ');
+ }).join(', ');
+};
+
+module.exports = { format, parse };
diff --git a/node_modules/ws/lib/PerMessageDeflate.js b/node_modules/ws/lib/PerMessageDeflate.js
new file mode 100644
index 0000000..c1a1d3c
--- /dev/null
+++ b/node_modules/ws/lib/PerMessageDeflate.js
@@ -0,0 +1,384 @@
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+const zlib = require('zlib');
+
+const bufferUtil = require('./BufferUtil');
+
+const Buffer = safeBuffer.Buffer;
+
+const AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15];
+const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
+const EMPTY_BLOCK = Buffer.from([0x00]);
+const DEFAULT_WINDOW_BITS = 15;
+const DEFAULT_MEM_LEVEL = 8;
+
+/**
+ * Per-message Deflate implementation.
+ */
+class PerMessageDeflate {
+ constructor (options, isServer, maxPayload) {
+ this._options = options || {};
+ this._isServer = !!isServer;
+ this._inflate = null;
+ this._deflate = null;
+ this.params = null;
+ this._maxPayload = maxPayload || 0;
+ this.threshold = this._options.threshold === undefined ? 1024 : this._options.threshold;
+ }
+
+ static get extensionName () {
+ return 'permessage-deflate';
+ }
+
+ /**
+ * Create extension parameters 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 extension offer.
+ *
+ * @param {Array} paramsList Extension parameters
+ * @return {Object} Accepted configuration
+ * @public
+ */
+ accept (paramsList) {
+ paramsList = this.normalizeParams(paramsList);
+
+ var params;
+ if (this._isServer) {
+ params = this.acceptAsServer(paramsList);
+ } else {
+ params = this.acceptAsClient(paramsList);
+ }
+
+ this.params = params;
+ return params;
+ }
+
+ /**
+ * Releases all resources used by the extension.
+ *
+ * @public
+ */
+ cleanup () {
+ if (this._inflate) {
+ if (this._inflate.writeInProgress) {
+ this._inflate.pendingClose = true;
+ } else {
+ this._inflate.close();
+ this._inflate = null;
+ }
+ }
+ if (this._deflate) {
+ if (this._deflate.writeInProgress) {
+ this._deflate.pendingClose = true;
+ } else {
+ this._deflate.close();
+ this._deflate = null;
+ }
+ }
+ }
+
+ /**
+ * Accept extension offer from client.
+ *
+ * @param {Array} paramsList Extension parameters
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsServer (paramsList) {
+ const accepted = {};
+ const result = paramsList.some((params) => {
+ if ((
+ this._options.serverNoContextTakeover === false &&
+ params.server_no_context_takeover
+ ) || (
+ this._options.serverMaxWindowBits === false &&
+ params.server_max_window_bits
+ ) || (
+ typeof this._options.serverMaxWindowBits === 'number' &&
+ typeof params.server_max_window_bits === 'number' &&
+ this._options.serverMaxWindowBits > params.server_max_window_bits
+ ) || (
+ typeof this._options.clientMaxWindowBits === 'number' &&
+ !params.client_max_window_bits
+ )) {
+ return;
+ }
+
+ if (
+ this._options.serverNoContextTakeover ||
+ params.server_no_context_takeover
+ ) {
+ accepted.server_no_context_takeover = true;
+ }
+ if (this._options.clientNoContextTakeover) {
+ accepted.client_no_context_takeover = true;
+ }
+ if (
+ this._options.clientNoContextTakeover !== false &&
+ params.client_no_context_takeover
+ ) {
+ accepted.client_no_context_takeover = true;
+ }
+ if (typeof this._options.serverMaxWindowBits === 'number') {
+ accepted.server_max_window_bits = this._options.serverMaxWindowBits;
+ } else if (typeof params.server_max_window_bits === 'number') {
+ accepted.server_max_window_bits = params.server_max_window_bits;
+ }
+ if (typeof this._options.clientMaxWindowBits === 'number') {
+ accepted.client_max_window_bits = this._options.clientMaxWindowBits;
+ } else if (
+ this._options.clientMaxWindowBits !== false &&
+ typeof params.client_max_window_bits === 'number'
+ ) {
+ accepted.client_max_window_bits = params.client_max_window_bits;
+ }
+ return true;
+ });
+
+ if (!result) throw new Error(`Doesn't support the offered configuration`);
+
+ return accepted;
+ }
+
+ /**
+ * Accept extension response from server.
+ *
+ * @param {Array} paramsList Extension parameters
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsClient (paramsList) {
+ const params = paramsList[0];
+
+ if (this._options.clientNoContextTakeover != null) {
+ if (
+ this._options.clientNoContextTakeover === false &&
+ params.client_no_context_takeover
+ ) {
+ throw new Error('Invalid value for "client_no_context_takeover"');
+ }
+ }
+ if (this._options.clientMaxWindowBits != null) {
+ if (
+ this._options.clientMaxWindowBits === false &&
+ params.client_max_window_bits
+ ) {
+ throw new Error('Invalid value for "client_max_window_bits"');
+ }
+ if (
+ typeof this._options.clientMaxWindowBits === 'number' && (
+ !params.client_max_window_bits ||
+ params.client_max_window_bits > this._options.clientMaxWindowBits
+ )) {
+ throw new Error('Invalid value for "client_max_window_bits"');
+ }
+ }
+
+ return params;
+ }
+
+ /**
+ * Normalize extensions parameters.
+ *
+ * @param {Array} paramsList Extension parameters
+ * @return {Array} Normalized extensions parameters
+ * @private
+ */
+ normalizeParams (paramsList) {
+ return paramsList.map((params) => {
+ Object.keys(params).forEach((key) => {
+ var value = params[key];
+ if (value.length > 1) {
+ throw new Error(`Multiple extension parameters for ${key}`);
+ }
+
+ value = value[0];
+
+ switch (key) {
+ case 'server_no_context_takeover':
+ case 'client_no_context_takeover':
+ if (value !== true) {
+ throw new Error(`invalid extension parameter value for ${key} (${value})`);
+ }
+ params[key] = true;
+ break;
+ case 'server_max_window_bits':
+ case 'client_max_window_bits':
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) {
+ throw new Error(`invalid extension parameter value for ${key} (${value})`);
+ }
+ }
+ if (!this._isServer && value === true) {
+ throw new Error(`Missing extension parameter value for ${key}`);
+ }
+ params[key] = value;
+ break;
+ default:
+ throw new Error(`Not defined extension parameter (${key})`);
+ }
+ });
+ return params;
+ });
+ }
+
+ /**
+ * Decompress data.
+ *
+ * @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) {
+ const endpoint = this._isServer ? 'client' : 'server';
+
+ if (!this._inflate) {
+ const maxWindowBits = this.params[`${endpoint}_max_window_bits`];
+ this._inflate = zlib.createInflateRaw({
+ windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS
+ });
+ }
+ this._inflate.writeInProgress = true;
+
+ var totalLength = 0;
+ const buffers = [];
+ var err;
+
+ const onData = (data) => {
+ totalLength += data.length;
+ if (this._maxPayload < 1 || totalLength <= this._maxPayload) {
+ return buffers.push(data);
+ }
+
+ err = new Error('max payload size exceeded');
+ err.closeCode = 1009;
+ this._inflate.reset();
+ };
+
+ const onError = (err) => {
+ cleanup();
+ callback(err);
+ };
+
+ const cleanup = () => {
+ if (!this._inflate) return;
+
+ this._inflate.removeListener('error', onError);
+ this._inflate.removeListener('data', onData);
+ this._inflate.writeInProgress = false;
+
+ if (
+ (fin && this.params[`${endpoint}_no_context_takeover`]) ||
+ this._inflate.pendingClose
+ ) {
+ this._inflate.close();
+ this._inflate = null;
+ }
+ };
+
+ this._inflate.on('error', onError).on('data', onData);
+ this._inflate.write(data);
+ if (fin) this._inflate.write(TRAILER);
+
+ this._inflate.flush(() => {
+ cleanup();
+ if (err) callback(err);
+ else callback(null, bufferUtil.concat(buffers, totalLength));
+ });
+ }
+
+ /**
+ * Compress data.
+ *
+ * @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) {
+ if (!data || data.length === 0) {
+ process.nextTick(callback, null, EMPTY_BLOCK);
+ return;
+ }
+
+ const endpoint = this._isServer ? 'server' : 'client';
+
+ if (!this._deflate) {
+ const maxWindowBits = this.params[`${endpoint}_max_window_bits`];
+ this._deflate = zlib.createDeflateRaw({
+ flush: zlib.Z_SYNC_FLUSH,
+ windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS,
+ memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL
+ });
+ }
+ this._deflate.writeInProgress = true;
+
+ var totalLength = 0;
+ const buffers = [];
+
+ const onData = (data) => {
+ totalLength += data.length;
+ buffers.push(data);
+ };
+
+ const onError = (err) => {
+ cleanup();
+ callback(err);
+ };
+
+ const cleanup = () => {
+ if (!this._deflate) return;
+
+ this._deflate.removeListener('error', onError);
+ this._deflate.removeListener('data', onData);
+ this._deflate.writeInProgress = false;
+
+ if (
+ (fin && this.params[`${endpoint}_no_context_takeover`]) ||
+ this._deflate.pendingClose
+ ) {
+ this._deflate.close();
+ this._deflate = null;
+ }
+ };
+
+ this._deflate.on('error', onError).on('data', onData);
+ this._deflate.write(data);
+ this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
+ cleanup();
+ var data = bufferUtil.concat(buffers, totalLength);
+ if (fin) data = data.slice(0, data.length - 4);
+ callback(null, data);
+ });
+ }
+}
+
+module.exports = PerMessageDeflate;
diff --git a/node_modules/ws/lib/Receiver.js b/node_modules/ws/lib/Receiver.js
new file mode 100644
index 0000000..6c1a10e
--- /dev/null
+++ b/node_modules/ws/lib/Receiver.js
@@ -0,0 +1,555 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+
+const PerMessageDeflate = require('./PerMessageDeflate');
+const isValidUTF8 = require('./Validation');
+const bufferUtil = require('./BufferUtil');
+const ErrorCodes = require('./ErrorCodes');
+const constants = require('./Constants');
+
+const Buffer = safeBuffer.Buffer;
+
+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.
+ */
+class Receiver {
+ /**
+ * Creates a Receiver instance.
+ *
+ * @param {Object} extensions An object containing the negotiated extensions
+ * @param {Number} maxPayload The maximum allowed message length
+ * @param {String} binaryType The type for binary data
+ */
+ constructor (extensions, maxPayload, binaryType) {
+ this.binaryType = binaryType || constants.BINARY_TYPES[0];
+ this.extensions = extensions || {};
+ this.maxPayload = maxPayload | 0;
+
+ this.bufferedBytes = 0;
+ this.buffers = [];
+
+ this.compressed = false;
+ this.payloadLength = 0;
+ this.fragmented = 0;
+ this.masked = false;
+ this.fin = false;
+ this.mask = null;
+ this.opcode = 0;
+
+ this.totalPayloadLength = 0;
+ this.messageLength = 0;
+ this.fragments = [];
+
+ this.cleanupCallback = null;
+ this.hadError = false;
+ this.dead = false;
+ this.loop = false;
+
+ this.onmessage = null;
+ this.onclose = null;
+ this.onerror = null;
+ this.onping = null;
+ this.onpong = null;
+
+ this.state = GET_INFO;
+ }
+
+ /**
+ * Consumes bytes from the available buffered data.
+ *
+ * @param {Number} bytes The number of bytes to consume
+ * @return {Buffer} Consumed bytes
+ * @private
+ */
+ readBuffer (bytes) {
+ var offset = 0;
+ var dst;
+ var l;
+
+ this.bufferedBytes -= bytes;
+
+ if (bytes === this.buffers[0].length) return this.buffers.shift();
+
+ if (bytes < this.buffers[0].length) {
+ dst = this.buffers[0].slice(0, bytes);
+ this.buffers[0] = this.buffers[0].slice(bytes);
+ return dst;
+ }
+
+ dst = Buffer.allocUnsafe(bytes);
+
+ while (bytes > 0) {
+ l = this.buffers[0].length;
+
+ if (bytes >= l) {
+ this.buffers[0].copy(dst, offset);
+ offset += l;
+ this.buffers.shift();
+ } else {
+ this.buffers[0].copy(dst, offset, 0, bytes);
+ this.buffers[0] = this.buffers[0].slice(bytes);
+ }
+
+ bytes -= l;
+ }
+
+ return dst;
+ }
+
+ /**
+ * Checks if the number of buffered bytes is bigger or equal than `n` and
+ * calls `cleanup` if necessary.
+ *
+ * @param {Number} n The number of bytes to check against
+ * @return {Boolean} `true` if `bufferedBytes >= n`, else `false`
+ * @private
+ */
+ hasBufferedBytes (n) {
+ if (this.bufferedBytes >= n) return true;
+
+ this.loop = false;
+ if (this.dead) this.cleanup(this.cleanupCallback);
+ return false;
+ }
+
+ /**
+ * Adds new data to the parser.
+ *
+ * @public
+ */
+ add (data) {
+ if (this.dead) return;
+
+ this.bufferedBytes += data.length;
+ this.buffers.push(data);
+ this.startLoop();
+ }
+
+ /**
+ * Starts the parsing loop.
+ *
+ * @private
+ */
+ startLoop () {
+ this.loop = true;
+
+ while (this.loop) {
+ switch (this.state) {
+ case GET_INFO:
+ this.getInfo();
+ break;
+ case GET_PAYLOAD_LENGTH_16:
+ this.getPayloadLength16();
+ break;
+ case GET_PAYLOAD_LENGTH_64:
+ this.getPayloadLength64();
+ break;
+ case GET_MASK:
+ this.getMask();
+ break;
+ case GET_DATA:
+ this.getData();
+ break;
+ default: // `INFLATING`
+ this.loop = false;
+ }
+ }
+ }
+
+ /**
+ * Reads the first two bytes of a frame.
+ *
+ * @private
+ */
+ getInfo () {
+ if (!this.hasBufferedBytes(2)) return;
+
+ const buf = this.readBuffer(2);
+
+ if ((buf[0] & 0x30) !== 0x00) {
+ this.error(new Error('RSV2 and RSV3 must be clear'), 1002);
+ return;
+ }
+
+ const compressed = (buf[0] & 0x40) === 0x40;
+
+ if (compressed && !this.extensions[PerMessageDeflate.extensionName]) {
+ this.error(new Error('RSV1 must be clear'), 1002);
+ return;
+ }
+
+ this.fin = (buf[0] & 0x80) === 0x80;
+ this.opcode = buf[0] & 0x0f;
+ this.payloadLength = buf[1] & 0x7f;
+
+ if (this.opcode === 0x00) {
+ if (compressed) {
+ this.error(new Error('RSV1 must be clear'), 1002);
+ return;
+ }
+
+ if (!this.fragmented) {
+ this.error(new Error(`invalid opcode: ${this.opcode}`), 1002);
+ return;
+ } else {
+ this.opcode = this.fragmented;
+ }
+ } else if (this.opcode === 0x01 || this.opcode === 0x02) {
+ if (this.fragmented) {
+ this.error(new Error(`invalid opcode: ${this.opcode}`), 1002);
+ return;
+ }
+
+ this.compressed = compressed;
+ } else if (this.opcode > 0x07 && this.opcode < 0x0b) {
+ if (!this.fin) {
+ this.error(new Error('FIN must be set'), 1002);
+ return;
+ }
+
+ if (compressed) {
+ this.error(new Error('RSV1 must be clear'), 1002);
+ return;
+ }
+
+ if (this.payloadLength > 0x7d) {
+ this.error(new Error('invalid payload length'), 1002);
+ return;
+ }
+ } else {
+ this.error(new Error(`invalid opcode: ${this.opcode}`), 1002);
+ return;
+ }
+
+ if (!this.fin && !this.fragmented) this.fragmented = this.opcode;
+
+ this.masked = (buf[1] & 0x80) === 0x80;
+
+ if (this.payloadLength === 126) this.state = GET_PAYLOAD_LENGTH_16;
+ else if (this.payloadLength === 127) this.state = GET_PAYLOAD_LENGTH_64;
+ else this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+16).
+ *
+ * @private
+ */
+ getPayloadLength16 () {
+ if (!this.hasBufferedBytes(2)) return;
+
+ this.payloadLength = this.readBuffer(2).readUInt16BE(0, true);
+ this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+64).
+ *
+ * @private
+ */
+ getPayloadLength64 () {
+ if (!this.hasBufferedBytes(8)) return;
+
+ const buf = this.readBuffer(8);
+ const num = buf.readUInt32BE(0, true);
+
+ //
+ // 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.error(new Error('max payload size exceeded'), 1009);
+ return;
+ }
+
+ this.payloadLength = (num * Math.pow(2, 32)) + buf.readUInt32BE(4, true);
+ this.haveLength();
+ }
+
+ /**
+ * Payload length has been read.
+ *
+ * @private
+ */
+ haveLength () {
+ if (this.opcode < 0x08 && this.maxPayloadExceeded(this.payloadLength)) {
+ return;
+ }
+
+ if (this.masked) this.state = GET_MASK;
+ else this.state = GET_DATA;
+ }
+
+ /**
+ * Reads mask bytes.
+ *
+ * @private
+ */
+ getMask () {
+ if (!this.hasBufferedBytes(4)) return;
+
+ this.mask = this.readBuffer(4);
+ this.state = GET_DATA;
+ }
+
+ /**
+ * Reads data bytes.
+ *
+ * @private
+ */
+ getData () {
+ var data = constants.EMPTY_BUFFER;
+
+ if (this.payloadLength) {
+ if (!this.hasBufferedBytes(this.payloadLength)) return;
+
+ data = this.readBuffer(this.payloadLength);
+ if (this.masked) bufferUtil.unmask(data, this.mask);
+ }
+
+ if (this.opcode > 0x07) {
+ this.controlMessage(data);
+ } else if (this.compressed) {
+ this.state = INFLATING;
+ this.decompress(data);
+ } else if (this.pushFragment(data)) {
+ this.dataMessage();
+ }
+ }
+
+ /**
+ * Decompresses data.
+ *
+ * @param {Buffer} data Compressed data
+ * @private
+ */
+ decompress (data) {
+ const extension = this.extensions[PerMessageDeflate.extensionName];
+
+ extension.decompress(data, this.fin, (err, buf) => {
+ if (err) {
+ this.error(err, err.closeCode === 1009 ? 1009 : 1007);
+ return;
+ }
+
+ if (this.pushFragment(buf)) this.dataMessage();
+ this.startLoop();
+ });
+ }
+
+ /**
+ * Handles a data message.
+ *
+ * @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) {
+ var data;
+
+ if (this.binaryType === 'nodebuffer') {
+ data = toBuffer(fragments, messageLength);
+ } else if (this.binaryType === 'arraybuffer') {
+ data = toArrayBuffer(toBuffer(fragments, messageLength));
+ } else {
+ data = fragments;
+ }
+
+ this.onmessage(data, { masked: this.masked, binary: true });
+ } else {
+ const buf = toBuffer(fragments, messageLength);
+
+ if (!isValidUTF8(buf)) {
+ this.error(new Error('invalid utf8 sequence'), 1007);
+ return;
+ }
+
+ this.onmessage(buf.toString(), { masked: this.masked });
+ }
+ }
+
+ this.state = GET_INFO;
+ }
+
+ /**
+ * Handles a control message.
+ *
+ * @param {Buffer} data Data to handle
+ * @private
+ */
+ controlMessage (data) {
+ if (this.opcode === 0x08) {
+ if (data.length === 0) {
+ this.onclose(1000, '', { masked: this.masked });
+ this.loop = false;
+ this.cleanup(this.cleanupCallback);
+ } else if (data.length === 1) {
+ this.error(new Error('invalid payload length'), 1002);
+ } else {
+ const code = data.readUInt16BE(0, true);
+
+ if (!ErrorCodes.isValidErrorCode(code)) {
+ this.error(new Error(`invalid status code: ${code}`), 1002);
+ return;
+ }
+
+ const buf = data.slice(2);
+
+ if (!isValidUTF8(buf)) {
+ this.error(new Error('invalid utf8 sequence'), 1007);
+ return;
+ }
+
+ this.onclose(code, buf.toString(), { masked: this.masked });
+ this.loop = false;
+ this.cleanup(this.cleanupCallback);
+ }
+
+ return;
+ }
+
+ const flags = { masked: this.masked, binary: true };
+
+ if (this.opcode === 0x09) this.onping(data, flags);
+ else this.onpong(data, flags);
+
+ this.state = GET_INFO;
+ }
+
+ /**
+ * Handles an error.
+ *
+ * @param {Error} err The error
+ * @param {Number} code Close code
+ * @private
+ */
+ error (err, code) {
+ this.onerror(err, code);
+ this.hadError = true;
+ this.loop = false;
+ this.cleanup(this.cleanupCallback);
+ }
+
+ /**
+ * Checks payload size, disconnects socket when it exceeds `maxPayload`.
+ *
+ * @param {Number} length Payload length
+ * @private
+ */
+ maxPayloadExceeded (length) {
+ if (length === 0 || this.maxPayload < 1) return false;
+
+ const fullLength = this.totalPayloadLength + length;
+
+ if (fullLength <= this.maxPayload) {
+ this.totalPayloadLength = fullLength;
+ return false;
+ }
+
+ this.error(new Error('max payload size exceeded'), 1009);
+ return true;
+ }
+
+ /**
+ * Appends a fragment in the fragments array after checking that the sum of
+ * fragment lengths does not exceed `maxPayload`.
+ *
+ * @param {Buffer} fragment The fragment to add
+ * @return {Boolean} `true` if `maxPayload` is not exceeded, else `false`
+ * @private
+ */
+ pushFragment (fragment) {
+ if (fragment.length === 0) return true;
+
+ const totalLength = this.messageLength + fragment.length;
+
+ if (this.maxPayload < 1 || totalLength <= this.maxPayload) {
+ this.messageLength = totalLength;
+ this.fragments.push(fragment);
+ return true;
+ }
+
+ this.error(new Error('max payload size exceeded'), 1009);
+ return false;
+ }
+
+ /**
+ * Releases resources used by the receiver.
+ *
+ * @param {Function} cb Callback
+ * @public
+ */
+ cleanup (cb) {
+ this.dead = true;
+
+ if (!this.hadError && (this.loop || this.state === INFLATING)) {
+ this.cleanupCallback = cb;
+ } else {
+ this.extensions = null;
+ this.fragments = null;
+ this.buffers = null;
+ this.mask = null;
+
+ this.cleanupCallback = null;
+ this.onmessage = null;
+ this.onclose = null;
+ this.onerror = null;
+ this.onping = null;
+ this.onpong = null;
+
+ if (cb) cb();
+ }
+ }
+}
+
+module.exports = Receiver;
+
+/**
+ * Makes a buffer from a list of fragments.
+ *
+ * @param {Buffer[]} fragments The list of fragments composing the message
+ * @param {Number} messageLength The length of the message
+ * @return {Buffer}
+ * @private
+ */
+function toBuffer (fragments, messageLength) {
+ if (fragments.length === 1) return fragments[0];
+ if (fragments.length > 1) return bufferUtil.concat(fragments, messageLength);
+ return constants.EMPTY_BUFFER;
+}
+
+/**
+ * Converts a buffer to an `ArrayBuffer`.
+ *
+ * @param {Buffer} The buffer to convert
+ * @return {ArrayBuffer} Converted buffer
+ */
+function toArrayBuffer (buf) {
+ if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
+ return buf.buffer;
+ }
+
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
+}
diff --git a/node_modules/ws/lib/Sender.js b/node_modules/ws/lib/Sender.js
new file mode 100644
index 0000000..79e68a5
--- /dev/null
+++ b/node_modules/ws/lib/Sender.js
@@ -0,0 +1,403 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+const crypto = require('crypto');
+
+const PerMessageDeflate = require('./PerMessageDeflate');
+const bufferUtil = require('./BufferUtil');
+const ErrorCodes = require('./ErrorCodes');
+
+const Buffer = safeBuffer.Buffer;
+
+/**
+ * HyBi Sender implementation.
+ */
+class Sender {
+ /**
+ * Creates a Sender instance.
+ *
+ * @param {net.Socket} socket The connection socket
+ * @param {Object} extensions An object containing the negotiated extensions
+ */
+ constructor (socket, extensions) {
+ this.perMessageDeflate = (extensions || {})[PerMessageDeflate.extensionName];
+ this._socket = socket;
+
+ this.firstFragment = true;
+ this.compress = false;
+
+ this.bufferedBytes = 0;
+ this.deflating = false;
+ this.queue = [];
+
+ this.onerror = null;
+ }
+
+ /**
+ * 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 Specifies whether `data` can be modified
+ * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
+ * @param {Boolean} options.mask Specifies whether or not to mask `data`
+ * @param {Boolean} options.rsv1 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 = data.length < 1024 || (options.mask && options.readOnly);
+ var offset = options.mask ? 6 : 2;
+ var 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;
+
+ if (payloadLength === 126) {
+ target.writeUInt16BE(data.length, 2, true);
+ } else if (payloadLength === 127) {
+ target.writeUInt32BE(0, 2, true);
+ target.writeUInt32BE(data.length, 6, true);
+ }
+
+ if (!options.mask) {
+ target[1] = payloadLength;
+ if (merge) {
+ data.copy(target, offset);
+ return [target];
+ }
+
+ return [target, data];
+ }
+
+ const mask = crypto.randomBytes(4);
+
+ target[1] = payloadLength | 0x80;
+ target[offset - 4] = mask[0];
+ target[offset - 3] = mask[1];
+ target[offset - 2] = mask[2];
+ target[offset - 1] = mask[3];
+
+ if (merge) {
+ bufferUtil.mask(data, mask, target, offset, data.length);
+ return [target];
+ }
+
+ bufferUtil.mask(data, mask, data, 0, data.length);
+ return [target, data];
+ }
+
+ /**
+ * Sends a close message to the other peer.
+ *
+ * @param {(Number|undefined)} code The status code component of the body
+ * @param {String} data The message component of the body
+ * @param {Boolean} mask Specifies whether or not to mask the message
+ * @param {Function} cb Callback
+ * @public
+ */
+ close (code, data, mask, cb) {
+ if (code !== undefined && (typeof code !== 'number' || !ErrorCodes.isValidErrorCode(code))) {
+ throw new Error('first argument must be a valid error code number');
+ }
+
+ const buf = Buffer.allocUnsafe(2 + (data ? Buffer.byteLength(data) : 0));
+
+ buf.writeUInt16BE(code || 1000, 0, true);
+ if (buf.length > 2) 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 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 Specifies whether or not to mask `data`
+ * @public
+ */
+ ping (data, mask) {
+ var readOnly = true;
+
+ if (!Buffer.isBuffer(data)) {
+ if (data instanceof ArrayBuffer) {
+ data = Buffer.from(data);
+ } else if (ArrayBuffer.isView(data)) {
+ data = viewToBuffer(data);
+ } else {
+ data = Buffer.from(data);
+ readOnly = false;
+ }
+ }
+
+ if (this.deflating) {
+ this.enqueue([this.doPing, data, mask, readOnly]);
+ } else {
+ this.doPing(data, mask, readOnly);
+ }
+ }
+
+ /**
+ * Frames and sends a ping message.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} mask Specifies whether or not to mask `data`
+ * @param {Boolean} readOnly Specifies whether `data` can be modified
+ * @private
+ */
+ doPing (data, mask, readOnly) {
+ this.sendFrame(Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x09,
+ mask,
+ readOnly
+ }));
+ }
+
+ /**
+ * Sends a pong message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} mask Specifies whether or not to mask `data`
+ * @public
+ */
+ pong (data, mask) {
+ var readOnly = true;
+
+ if (!Buffer.isBuffer(data)) {
+ if (data instanceof ArrayBuffer) {
+ data = Buffer.from(data);
+ } else if (ArrayBuffer.isView(data)) {
+ data = viewToBuffer(data);
+ } else {
+ data = Buffer.from(data);
+ readOnly = false;
+ }
+ }
+
+ if (this.deflating) {
+ this.enqueue([this.doPong, data, mask, readOnly]);
+ } else {
+ this.doPong(data, mask, readOnly);
+ }
+ }
+
+ /**
+ * Frames and sends a pong message.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} mask Specifies whether or not to mask `data`
+ * @param {Boolean} readOnly Specifies whether `data` can be modified
+ * @private
+ */
+ doPong (data, mask, readOnly) {
+ this.sendFrame(Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x0a,
+ mask,
+ readOnly
+ }));
+ }
+
+ /**
+ * Sends a data message to the other peer.
+ *
+ * @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 Specifies whether the fragment is the last one
+ * @param {Boolean} options.mask Specifies whether or not to mask `data`
+ * @param {Function} cb Callback
+ * @public
+ */
+ send (data, options, cb) {
+ var opcode = options.binary ? 2 : 1;
+ var rsv1 = options.compress;
+ var readOnly = true;
+
+ if (!Buffer.isBuffer(data)) {
+ if (data instanceof ArrayBuffer) {
+ data = Buffer.from(data);
+ } else if (ArrayBuffer.isView(data)) {
+ data = viewToBuffer(data);
+ } else {
+ data = Buffer.from(data);
+ readOnly = false;
+ }
+ }
+
+ if (this.firstFragment) {
+ this.firstFragment = false;
+ if (rsv1 && this.perMessageDeflate) {
+ rsv1 = data.length >= this.perMessageDeflate.threshold;
+ }
+ this.compress = rsv1;
+ } else {
+ rsv1 = false;
+ opcode = 0;
+ }
+
+ if (options.fin) this.firstFragment = true;
+
+ if (this.perMessageDeflate) {
+ const opts = {
+ fin: options.fin,
+ rsv1,
+ opcode,
+ mask: options.mask,
+ readOnly
+ };
+
+ if (this.deflating) {
+ this.enqueue([this.dispatch, data, this.compress, opts, cb]);
+ } else {
+ this.dispatch(data, this.compress, opts, cb);
+ }
+ } else {
+ this.sendFrame(Sender.frame(data, {
+ fin: options.fin,
+ rsv1: false,
+ opcode,
+ mask: options.mask,
+ readOnly
+ }), cb);
+ }
+ }
+
+ /**
+ * Dispatches a data message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} compress Specifies whether or not to compress `data`
+ * @param {Object} options Options object
+ * @param {Number} options.opcode The opcode
+ * @param {Boolean} options.readOnly Specifies whether `data` can be modified
+ * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
+ * @param {Boolean} options.mask Specifies whether or not to mask `data`
+ * @param {Boolean} options.rsv1 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;
+ }
+
+ this.deflating = true;
+ this.perMessageDeflate.compress(data, options.fin, (err, buf) => {
+ if (err) {
+ if (cb) cb(err);
+ else this.onerror(err);
+ return;
+ }
+
+ options.readOnly = false;
+ this.sendFrame(Sender.frame(buf, options), cb);
+ this.deflating = false;
+ this.dequeue();
+ });
+ }
+
+ /**
+ * Executes queued send operations.
+ *
+ * @private
+ */
+ dequeue () {
+ while (!this.deflating && this.queue.length) {
+ const params = this.queue.shift();
+
+ this.bufferedBytes -= params[1].length;
+ params[0].apply(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.write(list[0]);
+ this._socket.write(list[1], cb);
+ } else {
+ this._socket.write(list[0], cb);
+ }
+ }
+}
+
+module.exports = Sender;
+
+/**
+ * Converts an `ArrayBuffer` view into a buffer.
+ *
+ * @param {(DataView|TypedArray)} view The view to convert
+ * @return {Buffer} Converted view
+ * @private
+ */
+function viewToBuffer (view) {
+ const buf = Buffer.from(view.buffer);
+
+ if (view.byteLength !== view.buffer.byteLength) {
+ return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);
+ }
+
+ return buf;
+}
diff --git a/node_modules/ws/lib/Validation.js b/node_modules/ws/lib/Validation.js
new file mode 100644
index 0000000..fcb170f
--- /dev/null
+++ b/node_modules/ws/lib/Validation.js
@@ -0,0 +1,17 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+try {
+ const isValidUTF8 = require('utf-8-validate');
+
+ module.exports = typeof isValidUTF8 === 'object'
+ ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0
+ : isValidUTF8;
+} catch (e) /* istanbul ignore next */ {
+ module.exports = () => true;
+}
diff --git a/node_modules/ws/lib/WebSocket.js b/node_modules/ws/lib/WebSocket.js
new file mode 100644
index 0000000..41868d8
--- /dev/null
+++ b/node_modules/ws/lib/WebSocket.js
@@ -0,0 +1,712 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+const EventEmitter = require('events');
+const crypto = require('crypto');
+const Ultron = require('ultron');
+const https = require('https');
+const http = require('http');
+const url = require('url');
+
+const PerMessageDeflate = require('./PerMessageDeflate');
+const EventTarget = require('./EventTarget');
+const Extensions = require('./Extensions');
+const constants = require('./Constants');
+const Receiver = require('./Receiver');
+const Sender = require('./Sender');
+
+const protocolVersions = [8, 13];
+const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly.
+
+/**
+ * Class representing a WebSocket.
+ *
+ * @extends EventEmitter
+ */
+class WebSocket extends EventEmitter {
+ /**
+ * Create a new `WebSocket`.
+ *
+ * @param {String} address The URL to which to connect
+ * @param {(String|String[])} protocols The subprotocols
+ * @param {Object} options Connection options
+ */
+ constructor (address, protocols, options) {
+ super();
+
+ if (!protocols) {
+ protocols = [];
+ } else if (typeof protocols === 'string') {
+ protocols = [protocols];
+ } else if (!Array.isArray(protocols)) {
+ options = protocols;
+ protocols = [];
+ }
+
+ this.readyState = WebSocket.CONNECTING;
+ this.bytesReceived = 0;
+ this.extensions = {};
+ this.protocol = '';
+
+ this._binaryType = constants.BINARY_TYPES[0];
+ this._finalize = this.finalize.bind(this);
+ this._finalizeCalled = false;
+ this._closeMessage = null;
+ this._closeTimer = null;
+ this._closeCode = null;
+ this._receiver = null;
+ this._sender = null;
+ this._socket = null;
+ this._ultron = null;
+
+ if (Array.isArray(address)) {
+ initAsServerClient.call(this, address[0], address[1], address[2], options);
+ } else {
+ initAsClient.call(this, address, protocols, options);
+ }
+ }
+
+ get CONNECTING () { return WebSocket.CONNECTING; }
+ get CLOSING () { return WebSocket.CLOSING; }
+ get CLOSED () { return WebSocket.CLOSED; }
+ get OPEN () { return WebSocket.OPEN; }
+
+ /**
+ * @type {Number}
+ */
+ get bufferedAmount () {
+ var amount = 0;
+
+ if (this._socket) {
+ amount = this._socket.bufferSize + this._sender.bufferedBytes;
+ }
+ return amount;
+ }
+
+ /**
+ * 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 (constants.BINARY_TYPES.indexOf(type) < 0) return;
+
+ this._binaryType = type;
+
+ //
+ // Allow to change `binaryType` on the fly.
+ //
+ if (this._receiver) this._receiver.binaryType = type;
+ }
+
+ /**
+ * Set up the socket and the internal resources.
+ *
+ * @param {net.Socket} socket The network socket between the server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @private
+ */
+ setSocket (socket, head) {
+ socket.setTimeout(0);
+ socket.setNoDelay();
+
+ this._receiver = new Receiver(this.extensions, this.maxPayload, this.binaryType);
+ this._sender = new Sender(socket, this.extensions);
+ this._ultron = new Ultron(socket);
+ this._socket = socket;
+
+ // socket cleanup handlers
+ this._ultron.on('close', this._finalize);
+ this._ultron.on('error', this._finalize);
+ this._ultron.on('end', this._finalize);
+
+ // ensure that the head is added to the receiver
+ if (head && head.length > 0) {
+ socket.unshift(head);
+ head = null;
+ }
+
+ // subsequent packets are pushed to the receiver
+ this._ultron.on('data', (data) => {
+ this.bytesReceived += data.length;
+ this._receiver.add(data);
+ });
+
+ // receiver event handlers
+ this._receiver.onmessage = (data, flags) => this.emit('message', data, flags);
+ this._receiver.onping = (data, flags) => {
+ this.pong(data, !this._isServer, true);
+ this.emit('ping', data, flags);
+ };
+ this._receiver.onpong = (data, flags) => this.emit('pong', data, flags);
+ this._receiver.onclose = (code, reason) => {
+ this._closeMessage = reason;
+ this._closeCode = code;
+ this.close(code, reason);
+ };
+ this._receiver.onerror = (error, code) => {
+ // close the connection when the receiver reports a HyBi error code
+ this.close(code, '');
+ this.emit('error', error);
+ };
+
+ // sender event handlers
+ this._sender.onerror = (error) => {
+ this.close(1002, '');
+ this.emit('error', error);
+ };
+
+ this.readyState = WebSocket.OPEN;
+ this.emit('open');
+ }
+
+ /**
+ * Clean up and release internal resources.
+ *
+ * @param {(Boolean|Error)} Indicates whether or not an error occurred
+ * @private
+ */
+ finalize (error) {
+ if (this._finalizeCalled) return;
+
+ this.readyState = WebSocket.CLOSING;
+ this._finalizeCalled = true;
+
+ clearTimeout(this._closeTimer);
+ this._closeTimer = null;
+
+ //
+ // If the connection was closed abnormally (with an error), or if the close
+ // control frame was malformed or not received then the close code must be
+ // 1006.
+ //
+ if (error) this._closeCode = 1006;
+
+ if (this._socket) {
+ this._ultron.destroy();
+ this._socket.on('error', function onerror () {
+ this.destroy();
+ });
+
+ if (!error) this._socket.end();
+ else this._socket.destroy();
+
+ this._socket = null;
+ this._ultron = null;
+ }
+
+ if (this._sender) {
+ this._sender = this._sender.onerror = null;
+ }
+
+ if (this._receiver) {
+ this._receiver.cleanup(() => this.emitClose());
+ this._receiver = null;
+ } else {
+ this.emitClose();
+ }
+ }
+
+ /**
+ * Emit the `close` event.
+ *
+ * @private
+ */
+ emitClose () {
+ this.readyState = WebSocket.CLOSED;
+ this.emit('close', this._closeCode || 1006, this._closeMessage || '');
+
+ if (this.extensions[PerMessageDeflate.extensionName]) {
+ this.extensions[PerMessageDeflate.extensionName].cleanup();
+ }
+
+ this.extensions = null;
+
+ this.removeAllListeners();
+ this.on('error', constants.NOOP); // Catch all errors after this.
+ }
+
+ /**
+ * Pause the socket stream.
+ *
+ * @public
+ */
+ pause () {
+ if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+
+ this._socket.pause();
+ }
+
+ /**
+ * Resume the socket stream
+ *
+ * @public
+ */
+ resume () {
+ if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
+
+ this._socket.resume();
+ }
+
+ /**
+ * Start a closing handshake.
+ *
+ * @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) {
+ if (this._req && !this._req.aborted) {
+ this._req.abort();
+ this.emit('error', new Error('closed before the connection is established'));
+ this.finalize(true);
+ }
+ return;
+ }
+
+ if (this.readyState === WebSocket.CLOSING) {
+ if (this._closeCode && this._socket) this._socket.end();
+ return;
+ }
+
+ this.readyState = WebSocket.CLOSING;
+ this._sender.close(code, data, !this._isServer, (err) => {
+ if (err) this.emit('error', err);
+
+ if (this._socket) {
+ if (this._closeCode) this._socket.end();
+ //
+ // Ensure that the connection is cleaned up even when the closing
+ // handshake fails.
+ //
+ clearTimeout(this._closeTimer);
+ this._closeTimer = setTimeout(this._finalize, closeTimeout, true);
+ }
+ });
+ }
+
+ /**
+ * Send a ping message.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} mask Indicates whether or not to mask `data`
+ * @param {Boolean} failSilently Indicates whether or not to throw if `readyState` isn't `OPEN`
+ * @public
+ */
+ ping (data, mask, failSilently) {
+ if (this.readyState !== WebSocket.OPEN) {
+ if (failSilently) return;
+ throw new Error('not opened');
+ }
+
+ if (typeof data === 'number') data = data.toString();
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.ping(data || constants.EMPTY_BUFFER, mask);
+ }
+
+ /**
+ * Send a pong message.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} mask Indicates whether or not to mask `data`
+ * @param {Boolean} failSilently Indicates whether or not to throw if `readyState` isn't `OPEN`
+ * @public
+ */
+ pong (data, mask, failSilently) {
+ if (this.readyState !== WebSocket.OPEN) {
+ if (failSilently) return;
+ throw new Error('not opened');
+ }
+
+ if (typeof data === 'number') data = data.toString();
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.pong(data || constants.EMPTY_BUFFER, mask);
+ }
+
+ /**
+ * 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 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 (typeof options === 'function') {
+ cb = options;
+ options = {};
+ }
+
+ if (this.readyState !== WebSocket.OPEN) {
+ if (cb) cb(new Error('not opened'));
+ else throw new Error('not opened');
+ return;
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ const opts = Object.assign({
+ binary: typeof data !== 'string',
+ mask: !this._isServer,
+ compress: true,
+ fin: true
+ }, options);
+
+ if (!this.extensions[PerMessageDeflate.extensionName]) {
+ opts.compress = false;
+ }
+
+ this._sender.send(data || constants.EMPTY_BUFFER, opts, cb);
+ }
+
+ /**
+ * Forcibly close the connection.
+ *
+ * @public
+ */
+ terminate () {
+ if (this.readyState === WebSocket.CLOSED) return;
+ if (this.readyState === WebSocket.CONNECTING) {
+ if (this._req && !this._req.aborted) {
+ this._req.abort();
+ this.emit('error', new Error('closed before the connection is established'));
+ this.finalize(true);
+ }
+ return;
+ }
+
+ this.finalize(true);
+ }
+}
+
+WebSocket.CONNECTING = 0;
+WebSocket.OPEN = 1;
+WebSocket.CLOSING = 2;
+WebSocket.CLOSED = 3;
+
+//
+// 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}`, {
+ /**
+ * Return the listener of the event.
+ *
+ * @return {(Function|undefined)} The event listener or `undefined`
+ * @public
+ */
+ get () {
+ const listeners = this.listeners(method);
+ for (var i = 0; i < listeners.length; i++) {
+ if (listeners[i]._listener) return listeners[i]._listener;
+ }
+ },
+ /**
+ * Add a listener for the event.
+ *
+ * @param {Function} listener The listener to add
+ * @public
+ */
+ set (listener) {
+ const listeners = this.listeners(method);
+ for (var 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 = EventTarget.addEventListener;
+WebSocket.prototype.removeEventListener = EventTarget.removeEventListener;
+
+module.exports = WebSocket;
+
+/**
+ * Initialize a WebSocket server client.
+ *
+ * @param {http.IncomingMessage} req The request object
+ * @param {net.Socket} socket The network socket between the server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Object} options WebSocket attributes
+ * @param {Number} options.protocolVersion The WebSocket protocol version
+ * @param {Object} options.extensions The negotiated extensions
+ * @param {Number} options.maxPayload The maximum allowed message size
+ * @param {String} options.protocol The chosen subprotocol
+ * @private
+ */
+function initAsServerClient (req, socket, head, options) {
+ this.protocolVersion = options.protocolVersion;
+ this.extensions = options.extensions;
+ this.maxPayload = options.maxPayload;
+ this.protocol = options.protocol;
+
+ this.upgradeReq = req;
+ this._isServer = true;
+
+ this.setSocket(socket, head);
+}
+
+/**
+ * Initialize a WebSocket client.
+ *
+ * @param {String} address The URL to which to connect
+ * @param {String[]} protocols The list of subprotocols
+ * @param {Object} options Connection options
+ * @param {String} options.protocol Value of the `Sec-WebSocket-Protocol` header
+ * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate
+ * @param {String} options.localAddress Local interface to bind for network connections
+ * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` header
+ * @param {Object} options.headers An object containing request headers
+ * @param {String} options.origin Value of the `Origin` or `Sec-WebSocket-Origin` header
+ * @param {http.Agent} options.agent Use the specified Agent
+ * @param {String} options.host Value of the `Host` header
+ * @param {Number} options.family IP address family to use during hostname lookup (4 or 6).
+ * @param {Function} options.checkServerIdentity A function to validate the server hostname
+ * @param {Boolean} options.rejectUnauthorized Verify or not the server certificate
+ * @param {String} options.passphrase The passphrase for the private key or pfx
+ * @param {String} options.ciphers The ciphers to use or exclude
+ * @param {(String|String[]|Buffer|Buffer[])} options.cert The certificate key
+ * @param {(String|String[]|Buffer|Buffer[])} options.key The private key
+ * @param {(String|Buffer)} options.pfx The private key, certificate, and CA certs
+ * @param {(String|String[]|Buffer|Buffer[])} options.ca Trusted certificates
+ * @private
+ */
+function initAsClient (address, protocols, options) {
+ options = Object.assign({
+ protocolVersion: protocolVersions[1],
+ protocol: protocols.join(','),
+ perMessageDeflate: true,
+ localAddress: null,
+ headers: null,
+ family: null,
+ origin: null,
+ agent: null,
+ host: null,
+
+ //
+ // SSL options.
+ //
+ checkServerIdentity: null,
+ rejectUnauthorized: null,
+ passphrase: null,
+ ciphers: null,
+ cert: null,
+ key: null,
+ pfx: null,
+ ca: null
+ }, options);
+
+ if (protocolVersions.indexOf(options.protocolVersion) === -1) {
+ throw new Error(
+ `unsupported protocol version: ${options.protocolVersion} ` +
+ `(supported versions: ${protocolVersions.join(', ')})`
+ );
+ }
+
+ this.protocolVersion = options.protocolVersion;
+ this._isServer = false;
+ this.url = address;
+
+ const serverUrl = url.parse(address);
+ const isUnixSocket = serverUrl.protocol === 'ws+unix:';
+
+ if (!serverUrl.host && (!isUnixSocket || !serverUrl.path)) {
+ throw new Error('invalid url');
+ }
+
+ const isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:';
+ const key = crypto.randomBytes(16).toString('base64');
+ const httpObj = isSecure ? https : http;
+
+ //
+ // Prepare extensions.
+ //
+ const extensionsOffer = {};
+ var perMessageDeflate;
+
+ if (options.perMessageDeflate) {
+ perMessageDeflate = new PerMessageDeflate(
+ options.perMessageDeflate !== true ? options.perMessageDeflate : {},
+ false
+ );
+ extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer();
+ }
+
+ const requestOptions = {
+ port: serverUrl.port || (isSecure ? 443 : 80),
+ host: serverUrl.hostname,
+ path: '/',
+ headers: {
+ 'Sec-WebSocket-Version': options.protocolVersion,
+ 'Sec-WebSocket-Key': key,
+ 'Connection': 'Upgrade',
+ 'Upgrade': 'websocket'
+ }
+ };
+
+ if (options.headers) Object.assign(requestOptions.headers, options.headers);
+ if (Object.keys(extensionsOffer).length) {
+ requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer);
+ }
+ if (options.protocol) {
+ requestOptions.headers['Sec-WebSocket-Protocol'] = options.protocol;
+ }
+ if (options.origin) {
+ if (options.protocolVersion < 13) {
+ requestOptions.headers['Sec-WebSocket-Origin'] = options.origin;
+ } else {
+ requestOptions.headers.Origin = options.origin;
+ }
+ }
+ if (options.host) requestOptions.headers.Host = options.host;
+ if (serverUrl.auth) requestOptions.auth = serverUrl.auth;
+
+ if (options.localAddress) requestOptions.localAddress = options.localAddress;
+ if (options.family) requestOptions.family = options.family;
+
+ if (isUnixSocket) {
+ const parts = serverUrl.path.split(':');
+
+ requestOptions.socketPath = parts[0];
+ requestOptions.path = parts[1];
+ } else if (serverUrl.path) {
+ //
+ // Make sure that path starts with `/`.
+ //
+ if (serverUrl.path.charAt(0) !== '/') {
+ requestOptions.path = `/${serverUrl.path}`;
+ } else {
+ requestOptions.path = serverUrl.path;
+ }
+ }
+
+ var agent = options.agent;
+
+ //
+ // A custom agent is required for these options.
+ //
+ if (
+ options.rejectUnauthorized != null ||
+ options.checkServerIdentity ||
+ options.passphrase ||
+ options.ciphers ||
+ options.cert ||
+ options.key ||
+ options.pfx ||
+ options.ca
+ ) {
+ if (options.passphrase) requestOptions.passphrase = options.passphrase;
+ if (options.ciphers) requestOptions.ciphers = options.ciphers;
+ if (options.cert) requestOptions.cert = options.cert;
+ if (options.key) requestOptions.key = options.key;
+ if (options.pfx) requestOptions.pfx = options.pfx;
+ if (options.ca) requestOptions.ca = options.ca;
+ if (options.checkServerIdentity) {
+ requestOptions.checkServerIdentity = options.checkServerIdentity;
+ }
+ if (options.rejectUnauthorized != null) {
+ requestOptions.rejectUnauthorized = options.rejectUnauthorized;
+ }
+
+ if (!agent) agent = new httpObj.Agent(requestOptions);
+ }
+
+ if (agent) requestOptions.agent = agent;
+
+ this._req = httpObj.get(requestOptions);
+
+ this._req.on('error', (error) => {
+ if (this._req.aborted) return;
+
+ this._req = null;
+ this.emit('error', error);
+ this.finalize(true);
+ });
+
+ this._req.on('response', (res) => {
+ if (!this.emit('unexpected-response', this._req, res)) {
+ this._req.abort();
+ this.emit('error', new Error(`unexpected server response (${res.statusCode})`));
+ this.finalize(true);
+ }
+ });
+
+ this._req.on('upgrade', (res, socket, head) => {
+ this.emit('headers', res.headers, res);
+
+ //
+ // The user may have closed the connection from a listener of the `headers`
+ // event.
+ //
+ if (this.readyState !== WebSocket.CONNECTING) return;
+
+ this._req = null;
+
+ const digest = crypto.createHash('sha1')
+ .update(key + constants.GUID, 'binary')
+ .digest('base64');
+
+ if (res.headers['sec-websocket-accept'] !== digest) {
+ socket.destroy();
+ this.emit('error', new Error('invalid server key'));
+ return this.finalize(true);
+ }
+
+ const serverProt = res.headers['sec-websocket-protocol'];
+ const protList = (options.protocol || '').split(/, */);
+ var protError;
+
+ if (!options.protocol && serverProt) {
+ protError = 'server sent a subprotocol even though none requested';
+ } else if (options.protocol && !serverProt) {
+ protError = 'server sent no subprotocol even though requested';
+ } else if (serverProt && protList.indexOf(serverProt) === -1) {
+ protError = 'server responded with an invalid protocol';
+ }
+
+ if (protError) {
+ socket.destroy();
+ this.emit('error', new Error(protError));
+ return this.finalize(true);
+ }
+
+ if (serverProt) this.protocol = serverProt;
+
+ const serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']);
+
+ if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) {
+ try {
+ perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]);
+ } catch (err) {
+ socket.destroy();
+ this.emit('error', new Error('invalid extension parameter'));
+ return this.finalize(true);
+ }
+
+ this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+ }
+
+ this.setSocket(socket, head);
+ });
+}
diff --git a/node_modules/ws/lib/WebSocketServer.js b/node_modules/ws/lib/WebSocketServer.js
new file mode 100644
index 0000000..bd3ef24
--- /dev/null
+++ b/node_modules/ws/lib/WebSocketServer.js
@@ -0,0 +1,336 @@
+/*!
+ * ws: a node.js websocket client
+ * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+ * MIT Licensed
+ */
+
+'use strict';
+
+const safeBuffer = require('safe-buffer');
+const EventEmitter = require('events');
+const crypto = require('crypto');
+const Ultron = require('ultron');
+const http = require('http');
+const url = require('url');
+
+const PerMessageDeflate = require('./PerMessageDeflate');
+const Extensions = require('./Extensions');
+const constants = require('./Constants');
+const WebSocket = require('./WebSocket');
+
+const Buffer = safeBuffer.Buffer;
+
+/**
+ * Class representing a WebSocket server.
+ *
+ * @extends EventEmitter
+ */
+class WebSocketServer extends EventEmitter {
+ /**
+ * Create a `WebSocketServer` instance.
+ *
+ * @param {Object} options Configuration options
+ * @param {String} options.host The hostname where to bind the server
+ * @param {Number} options.port The port where to bind the server
+ * @param {http.Server} options.server A pre-created HTTP/S server to use
+ * @param {Function} options.verifyClient An hook to reject connections
+ * @param {Function} options.handleProtocols An hook to handle protocols
+ * @param {String} options.path Accept only connections matching this path
+ * @param {Boolean} options.noServer Enable no server mode
+ * @param {Boolean} options.clientTracking Specifies whether or not to track clients
+ * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate
+ * @param {Number} options.maxPayload The maximum allowed message size
+ * @param {Function} callback A listener for the `listening` event
+ */
+ constructor (options, callback) {
+ super();
+
+ options = Object.assign({
+ maxPayload: 100 * 1024 * 1024,
+ perMessageDeflate: true,
+ 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) {
+ throw new TypeError('missing or invalid options');
+ }
+
+ 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.allowHalfOpen = false;
+ this._server.listen(options.port, options.host, options.backlog, callback);
+ } else if (options.server) {
+ this._server = options.server;
+ }
+
+ if (this._server) {
+ this._ultron = new Ultron(this._server);
+ this._ultron.on('listening', () => this.emit('listening'));
+ this._ultron.on('error', (err) => this.emit('error', err));
+ this._ultron.on('upgrade', (req, socket, head) => {
+ this.handleUpgrade(req, socket, head, (client) => {
+ this.emit(`connection${req.url}`, client);
+ this.emit('connection', client);
+ });
+ });
+ }
+
+ if (options.clientTracking) this.clients = new Set();
+ this.options = options;
+ this.path = options.path;
+ }
+
+ /**
+ * Close the server.
+ *
+ * @param {Function} cb Callback
+ * @public
+ */
+ close (cb) {
+ //
+ // Terminate all associated clients.
+ //
+ if (this.clients) {
+ for (const client of this.clients) client.terminate();
+ }
+
+ const server = this._server;
+
+ if (server) {
+ this._ultron.destroy();
+ this._ultron = this._server = null;
+
+ //
+ // Close the http server if it was internally created.
+ //
+ if (this.options.port != null) return server.close(cb);
+ }
+
+ if (cb) cb();
+ }
+
+ /**
+ * 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 && url.parse(req.url).pathname !== this.options.path) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle a HTTP Upgrade request.
+ *
+ * @param {http.IncomingMessage} req The request object
+ * @param {net.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', socketError);
+
+ const version = +req.headers['sec-websocket-version'];
+
+ if (
+ req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' ||
+ !req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) ||
+ !this.shouldHandle(req)
+ ) {
+ return abortConnection(socket, 400);
+ }
+
+ var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */);
+
+ //
+ // Optionally call external protocol selection handler.
+ //
+ if (this.options.handleProtocols) {
+ protocol = this.options.handleProtocols(protocol, req);
+ if (protocol === false) return abortConnection(socket, 401);
+ } else {
+ protocol = protocol[0];
+ }
+
+ //
+ // Optionally call external client verification handler.
+ //
+ if (this.options.verifyClient) {
+ const info = {
+ origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
+ secure: !!(req.connection.authorized || req.connection.encrypted),
+ req
+ };
+
+ if (this.options.verifyClient.length === 2) {
+ this.options.verifyClient(info, (verified, code, message) => {
+ if (!verified) return abortConnection(socket, code || 401, message);
+
+ this.completeUpgrade(protocol, version, req, socket, head, cb);
+ });
+ return;
+ } else if (!this.options.verifyClient(info)) {
+ return abortConnection(socket, 401);
+ }
+ }
+
+ this.completeUpgrade(protocol, version, req, socket, head, cb);
+ }
+
+ /**
+ * Upgrade the connection to WebSocket.
+ *
+ * @param {String} protocol The chosen subprotocol
+ * @param {Number} version The WebSocket protocol version
+ * @param {http.IncomingMessage} req The request object
+ * @param {net.Socket} socket The network socket between the server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @private
+ */
+ completeUpgrade (protocol, version, req, socket, head, cb) {
+ //
+ // Destroy the socket if the client has already sent a FIN packet.
+ //
+ if (!socket.readable || !socket.writable) return socket.destroy();
+
+ const key = crypto.createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
+ .digest('base64');
+
+ const headers = [
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade',
+ `Sec-WebSocket-Accept: ${key}`
+ ];
+
+ if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
+
+ const offer = Extensions.parse(req.headers['sec-websocket-extensions']);
+ var extensions;
+
+ try {
+ extensions = acceptExtensions(this.options, offer);
+ } catch (err) {
+ return abortConnection(socket, 400);
+ }
+
+ const props = Object.keys(extensions);
+
+ if (props.length) {
+ const serverExtensions = props.reduce((obj, key) => {
+ obj[key] = [extensions[key].params];
+ return obj;
+ }, {});
+
+ headers.push(`Sec-WebSocket-Extensions: ${Extensions.format(serverExtensions)}`);
+ }
+
+ //
+ // Allow external modification/inspection of handshake headers.
+ //
+ this.emit('headers', headers, req);
+
+ socket.write(headers.concat('', '').join('\r\n'));
+
+ const client = new WebSocket([req, socket, head], null, {
+ maxPayload: this.options.maxPayload,
+ protocolVersion: version,
+ extensions,
+ protocol
+ });
+
+ if (this.clients) {
+ this.clients.add(client);
+ client.on('close', () => this.clients.delete(client));
+ }
+
+ socket.removeListener('error', socketError);
+ cb(client);
+ }
+}
+
+module.exports = WebSocketServer;
+
+/**
+ * Handle premature socket errors.
+ *
+ * @private
+ */
+function socketError () {
+ this.destroy();
+}
+
+/**
+ * Accept WebSocket extensions.
+ *
+ * @param {Object} options The `WebSocketServer` configuration options
+ * @param {Object} offer The parsed value of the `sec-websocket-extensions` header
+ * @return {Object} Accepted extensions
+ * @private
+ */
+function acceptExtensions (options, offer) {
+ const pmd = options.perMessageDeflate;
+ const extensions = {};
+
+ if (pmd && offer[PerMessageDeflate.extensionName]) {
+ const perMessageDeflate = new PerMessageDeflate(
+ pmd !== true ? pmd : {},
+ true,
+ options.maxPayload
+ );
+
+ perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
+ extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+ }
+
+ return extensions;
+}
+
+/**
+ * Close the connection when preconditions are not fulfilled.
+ *
+ * @param {net.Socket} socket The socket of the upgrade request
+ * @param {Number} code The HTTP response status code
+ * @param {String} [message] The HTTP response body
+ * @private
+ */
+function abortConnection (socket, code, message) {
+ if (socket.writable) {
+ message = message || http.STATUS_CODES[code];
+ socket.write(
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
+ 'Connection: close\r\n' +
+ 'Content-type: text/html\r\n' +
+ `Content-Length: ${Buffer.byteLength(message)}\r\n` +
+ '\r\n' +
+ message
+ );
+ }
+
+ socket.removeListener('error', socketError);
+ socket.destroy();
+}