diff options
author | Yaroslav De La Peña Smirnov <yaros.rus_89@live.com.mx> | 2017-11-29 11:44:34 +0300 |
---|---|---|
committer | Yaroslav De La Peña Smirnov <yaros.rus_89@live.com.mx> | 2017-11-29 11:44:34 +0300 |
commit | 67fdec20726e48ba3a934cb25bb30d47ec4a4f29 (patch) | |
tree | 37fd9f4f0b0c20103e1646fc83021e4765de3680 /node_modules/ws/lib | |
download | spanish-checkers-67fdec20726e48ba3a934cb25bb30d47ec4a4f29.tar.gz spanish-checkers-67fdec20726e48ba3a934cb25bb30d47ec4a4f29.zip |
Initial commit, version 0.5.3
Diffstat (limited to 'node_modules/ws/lib')
-rw-r--r-- | node_modules/ws/lib/BufferUtil.js | 71 | ||||
-rw-r--r-- | node_modules/ws/lib/Constants.js | 10 | ||||
-rw-r--r-- | node_modules/ws/lib/ErrorCodes.js | 28 | ||||
-rw-r--r-- | node_modules/ws/lib/EventTarget.js | 155 | ||||
-rw-r--r-- | node_modules/ws/lib/Extensions.js | 67 | ||||
-rw-r--r-- | node_modules/ws/lib/PerMessageDeflate.js | 384 | ||||
-rw-r--r-- | node_modules/ws/lib/Receiver.js | 555 | ||||
-rw-r--r-- | node_modules/ws/lib/Sender.js | 403 | ||||
-rw-r--r-- | node_modules/ws/lib/Validation.js | 17 | ||||
-rw-r--r-- | node_modules/ws/lib/WebSocket.js | 712 | ||||
-rw-r--r-- | node_modules/ws/lib/WebSocketServer.js | 336 |
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(); +} |