diff options
Diffstat (limited to 'node_modules/ws')
22 files changed, 4031 insertions, 0 deletions
diff --git a/node_modules/ws/LICENSE b/node_modules/ws/LICENSE new file mode 100644 index 0000000..a145cd1 --- /dev/null +++ b/node_modules/ws/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/ws/README.md b/node_modules/ws/README.md new file mode 100644 index 0000000..1ca0bdb --- /dev/null +++ b/node_modules/ws/README.md @@ -0,0 +1,260 @@ +# ws: a Node.js WebSocket library + +[![Version npm](https://img.shields.io/npm/v/ws.svg)](https://www.npmjs.com/package/ws) +[![Linux Build](https://img.shields.io/travis/websockets/ws/master.svg)](https://travis-ci.org/websockets/ws) +[![Windows Build](https://ci.appveyor.com/api/projects/status/github/websockets/ws?branch=master&svg=true)](https://ci.appveyor.com/project/lpinca/ws) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/r/websockets/ws?branch=master) + +`ws` is a simple to use, blazing fast, and thoroughly tested WebSocket client +and server implementation. + +Passes the quite extensive Autobahn test suite. See http://websockets.github.io/ws/ +for the full reports. + +**Note**: This module does not work in the browser. The client in the docs is a +reference to a back end with the role of a client in the WebSocket +communication. Browser clients must use the native +[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. + +## Protocol support + +* **HyBi drafts 07-12** (Use the option `protocolVersion: 8`) +* **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`) + +## Installing + +``` +npm install --save ws +``` + +### Opt-in for performance and spec compliance + +There are 2 optional modules that can be installed along side with the `ws` +module. These modules are binary addons which improve certain operations. +Prebuilt binaries are available for the most popular platforms so you don't +necessarily need to have a C++ compiler installed on your machine. + +- `npm install --save-optional bufferutil`: Allows to efficiently perform + operations such as masking and unmasking the data payload of the WebSocket + frames. +- `npm install --save-optional utf-8-validate`: Allows to efficiently check + if a message contains valid UTF-8 as required by the spec. + +## API Docs + +See [`/doc/ws.md`](https://github.com/websockets/ws/blob/master/doc/ws.md) +for Node.js-like docs for the ws classes. + +## WebSocket compression + +`ws` supports the [permessage-deflate extension][permessage-deflate] which +enables the client and server to negotiate a compression algorithm and its +parameters, and then selectively apply it to the data payloads of each +WebSocket message. + +The extension is enabled by default but adds a significant overhead in terms of +performance and memory comsumption. We suggest to use WebSocket compression +only if it is really needed. + +To disable the extension you can set the `perMessageDeflate` option to `false`. +On the server: + +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ + perMessageDeflate: false, + port: 8080 +}); +``` + +On the client: + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://www.host.com/path', { + perMessageDeflate: false +}); +``` + +## Usage examples + +### Sending and receiving text data + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://www.host.com/path'); + +ws.on('open', function open() { + ws.send('something'); +}); + +ws.on('message', function incoming(data, flags) { + // flags.binary will be set if a binary data is received. + // flags.masked will be set if the data was masked. +}); +``` + +### Sending binary data + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://www.host.com/path'); + +ws.on('open', function open() { + const array = new Float32Array(5); + + for (var i = 0; i < array.length; ++i) { + array[i] = i / 2; + } + + ws.send(array); +}); +``` + +### Server example + +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); + +wss.on('connection', function connection(ws) { + ws.on('message', function incoming(message) { + console.log('received: %s', message); + }); + + ws.send('something'); +}); +``` + +### Broadcast example + +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); + +// Broadcast to all. +wss.broadcast = function broadcast(data) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); +}; + +wss.on('connection', function connection(ws) { + ws.on('message', function incoming(data) { + // Broadcast to everyone else. + wss.clients.forEach(function each(client) { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); + }); +}); +``` + +### ExpressJS example + +```js +const express = require('express'); +const http = require('http'); +const url = require('url'); +const WebSocket = require('ws'); + +const app = express(); + +app.use(function (req, res) { + res.send({ msg: "hello" }); +}); + +const server = http.createServer(app); +const wss = new WebSocket.Server({ server }); + +wss.on('connection', function connection(ws) { + const location = url.parse(ws.upgradeReq.url, true); + // You might use location.query.access_token to authenticate or share sessions + // or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312) + + ws.on('message', function incoming(message) { + console.log('received: %s', message); + }); + + ws.send('something'); +}); + +server.listen(8080, function listening() { + console.log('Listening on %d', server.address().port); +}); +``` + +### echo.websocket.org demo + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://websocket.org' +}); + +ws.on('open', function open() { + console.log('connected'); + ws.send(Date.now()); +}); + +ws.on('close', function close() { + console.log('disconnected'); +}); + +ws.on('message', function incoming(data, flags) { + console.log(`Roundtrip time: ${Date.now() - data} ms`, flags); + + setTimeout(function timeout() { + ws.send(Date.now()); + }, 500); +}); +``` + +### Other examples + +For a full example with a browser client communicating with a ws server, see the +examples folder. + +Otherwise, see the test cases. + +## Error handling best practices + +```js +// If the WebSocket is closed before the following send is attempted +ws.send('something'); + +// Errors (both immediate and async write errors) can be detected in an optional +// callback. The callback is also the only way of being notified that data has +// actually been sent. +ws.send('something', function ack(error) { + // If error is not defined, the send has been completed, otherwise the error + // object will indicate what failed. +}); + +// Immediate errors can also be handled with `try...catch`, but **note** that +// since sends are inherently asynchronous, socket write failures will *not* be +// captured when this technique is used. +try { ws.send('something'); } +catch (e) { /* handle error */ } +``` + +## Changelog + +We're using the GitHub [`releases`](https://github.com/websockets/ws/releases) +for changelog entries. + +## License + +[MIT](LICENSE) + +[permessage-deflate]: https://tools.ietf.org/html/rfc7692 diff --git a/node_modules/ws/index.js b/node_modules/ws/index.js new file mode 100644 index 0000000..489e169 --- /dev/null +++ b/node_modules/ws/index.js @@ -0,0 +1,15 @@ +/*! + * ws: a node.js websocket client + * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com> + * MIT Licensed + */ + +'use strict'; + +const WebSocket = require('./lib/WebSocket'); + +WebSocket.Server = require('./lib/WebSocketServer'); +WebSocket.Receiver = require('./lib/Receiver'); +WebSocket.Sender = require('./lib/Sender'); + +module.exports = WebSocket; 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(); +} diff --git a/node_modules/ws/node_modules/safe-buffer/.travis.yml b/node_modules/ws/node_modules/safe-buffer/.travis.yml new file mode 100644 index 0000000..7b20f28 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - 'node' + - '5' + - '4' + - '0.12' + - '0.10' diff --git a/node_modules/ws/node_modules/safe-buffer/LICENSE b/node_modules/ws/node_modules/safe-buffer/LICENSE new file mode 100644 index 0000000..0c068ce --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node_modules/ws/node_modules/safe-buffer/README.md b/node_modules/ws/node_modules/safe-buffer/README.md new file mode 100644 index 0000000..96eb387 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/README.md @@ -0,0 +1,581 @@ +# safe-buffer [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][npm-url] + +#### Safer Node.js Buffer API + +**Use the new Node.js v6 Buffer APIs (`Buffer.from`, `Buffer.alloc`, +`Buffer.allocUnsafe`, `Buffer.allocUnsafeSlow`) in Node.js v0.10, v0.12, v4.x, and v5.x.** + +**Uses the built-in implementations when available.** + +[travis-image]: https://img.shields.io/travis/feross/safe-buffer.svg +[travis-url]: https://travis-ci.org/feross/safe-buffer +[npm-image]: https://img.shields.io/npm/v/safe-buffer.svg +[npm-url]: https://npmjs.org/package/safe-buffer +[downloads-image]: https://img.shields.io/npm/dm/safe-buffer.svg + +## install + +``` +npm install safe-buffer +``` + +## usage + +The goal of this package is to provide a safe replacement for the node.js `Buffer`. + +It's a drop-in replacement for `Buffer`. You can use it by adding one `require` line to +the top of your node.js modules: + +```js +var Buffer = require('safe-buffer').Buffer + +// Existing buffer code will continue to work without issues: + +new Buffer('hey', 'utf8') +new Buffer([1, 2, 3], 'utf8') +new Buffer(obj) +new Buffer(16) // create an uninitialized buffer (potentially unsafe) + +// But you can use these new explicit APIs to make clear what you want: + +Buffer.from('hey', 'utf8') // convert from many types to a Buffer +Buffer.alloc(16) // create a zero-filled buffer (safe) +Buffer.allocUnsafe(16) // create an uninitialized buffer (potentially unsafe) +``` + +## api + +### Class Method: Buffer.from(array) +<!-- YAML +added: v3.0.0 +--> + +* `array` {Array} + +Allocates a new `Buffer` using an `array` of octets. + +```js +const buf = Buffer.from([0x62,0x75,0x66,0x66,0x65,0x72]); + // creates a new Buffer containing ASCII bytes + // ['b','u','f','f','e','r'] +``` + +A `TypeError` will be thrown if `array` is not an `Array`. + +### Class Method: Buffer.from(arrayBuffer[, byteOffset[, length]]) +<!-- YAML +added: v5.10.0 +--> + +* `arrayBuffer` {ArrayBuffer} The `.buffer` property of a `TypedArray` or + a `new ArrayBuffer()` +* `byteOffset` {Number} Default: `0` +* `length` {Number} Default: `arrayBuffer.length - byteOffset` + +When passed a reference to the `.buffer` property of a `TypedArray` instance, +the newly created `Buffer` will share the same allocated memory as the +TypedArray. + +```js +const arr = new Uint16Array(2); +arr[0] = 5000; +arr[1] = 4000; + +const buf = Buffer.from(arr.buffer); // shares the memory with arr; + +console.log(buf); + // Prints: <Buffer 88 13 a0 0f> + +// changing the TypedArray changes the Buffer also +arr[1] = 6000; + +console.log(buf); + // Prints: <Buffer 88 13 70 17> +``` + +The optional `byteOffset` and `length` arguments specify a memory range within +the `arrayBuffer` that will be shared by the `Buffer`. + +```js +const ab = new ArrayBuffer(10); +const buf = Buffer.from(ab, 0, 2); +console.log(buf.length); + // Prints: 2 +``` + +A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer`. + +### Class Method: Buffer.from(buffer) +<!-- YAML +added: v3.0.0 +--> + +* `buffer` {Buffer} + +Copies the passed `buffer` data onto a new `Buffer` instance. + +```js +const buf1 = Buffer.from('buffer'); +const buf2 = Buffer.from(buf1); + +buf1[0] = 0x61; +console.log(buf1.toString()); + // 'auffer' +console.log(buf2.toString()); + // 'buffer' (copy is not changed) +``` + +A `TypeError` will be thrown if `buffer` is not a `Buffer`. + +### Class Method: Buffer.from(str[, encoding]) +<!-- YAML +added: v5.10.0 +--> + +* `str` {String} String to encode. +* `encoding` {String} Encoding to use, Default: `'utf8'` + +Creates a new `Buffer` containing the given JavaScript string `str`. If +provided, the `encoding` parameter identifies the character encoding. +If not provided, `encoding` defaults to `'utf8'`. + +```js +const buf1 = Buffer.from('this is a tést'); +console.log(buf1.toString()); + // prints: this is a tést +console.log(buf1.toString('ascii')); + // prints: this is a tC)st + +const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); +console.log(buf2.toString()); + // prints: this is a tést +``` + +A `TypeError` will be thrown if `str` is not a string. + +### Class Method: Buffer.alloc(size[, fill[, encoding]]) +<!-- YAML +added: v5.10.0 +--> + +* `size` {Number} +* `fill` {Value} Default: `undefined` +* `encoding` {String} Default: `utf8` + +Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the +`Buffer` will be *zero-filled*. + +```js +const buf = Buffer.alloc(5); +console.log(buf); + // <Buffer 00 00 00 00 00> +``` + +The `size` must be less than or equal to the value of +`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is +`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will +be created if a `size` less than or equal to 0 is specified. + +If `fill` is specified, the allocated `Buffer` will be initialized by calling +`buf.fill(fill)`. See [`buf.fill()`][] for more information. + +```js +const buf = Buffer.alloc(5, 'a'); +console.log(buf); + // <Buffer 61 61 61 61 61> +``` + +If both `fill` and `encoding` are specified, the allocated `Buffer` will be +initialized by calling `buf.fill(fill, encoding)`. For example: + +```js +const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); +console.log(buf); + // <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64> +``` + +Calling `Buffer.alloc(size)` can be significantly slower than the alternative +`Buffer.allocUnsafe(size)` but ensures that the newly created `Buffer` instance +contents will *never contain sensitive data*. + +A `TypeError` will be thrown if `size` is not a number. + +### Class Method: Buffer.allocUnsafe(size) +<!-- YAML +added: v5.10.0 +--> + +* `size` {Number} + +Allocates a new *non-zero-filled* `Buffer` of `size` bytes. The `size` must +be less than or equal to the value of `require('buffer').kMaxLength` (on 64-bit +architectures, `kMaxLength` is `(2^31)-1`). Otherwise, a [`RangeError`][] is +thrown. A zero-length Buffer will be created if a `size` less than or equal to +0 is specified. + +The underlying memory for `Buffer` instances created in this way is *not +initialized*. The contents of the newly created `Buffer` are unknown and +*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such +`Buffer` instances to zeroes. + +```js +const buf = Buffer.allocUnsafe(5); +console.log(buf); + // <Buffer 78 e0 82 02 01> + // (octets will be different, every time) +buf.fill(0); +console.log(buf); + // <Buffer 00 00 00 00 00> +``` + +A `TypeError` will be thrown if `size` is not a number. + +Note that the `Buffer` module pre-allocates an internal `Buffer` instance of +size `Buffer.poolSize` that is used as a pool for the fast allocation of new +`Buffer` instances created using `Buffer.allocUnsafe(size)` (and the deprecated +`new Buffer(size)` constructor) only when `size` is less than or equal to +`Buffer.poolSize >> 1` (floor of `Buffer.poolSize` divided by two). The default +value of `Buffer.poolSize` is `8192` but can be modified. + +Use of this pre-allocated internal memory pool is a key difference between +calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`. +Specifically, `Buffer.alloc(size, fill)` will *never* use the internal Buffer +pool, while `Buffer.allocUnsafe(size).fill(fill)` *will* use the internal +Buffer pool if `size` is less than or equal to half `Buffer.poolSize`. The +difference is subtle but can be important when an application requires the +additional performance that `Buffer.allocUnsafe(size)` provides. + +### Class Method: Buffer.allocUnsafeSlow(size) +<!-- YAML +added: v5.10.0 +--> + +* `size` {Number} + +Allocates a new *non-zero-filled* and non-pooled `Buffer` of `size` bytes. The +`size` must be less than or equal to the value of +`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is +`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will +be created if a `size` less than or equal to 0 is specified. + +The underlying memory for `Buffer` instances created in this way is *not +initialized*. The contents of the newly created `Buffer` are unknown and +*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such +`Buffer` instances to zeroes. + +When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, +allocations under 4KB are, by default, sliced from a single pre-allocated +`Buffer`. This allows applications to avoid the garbage collection overhead of +creating many individually allocated Buffers. This approach improves both +performance and memory usage by eliminating the need to track and cleanup as +many `Persistent` objects. + +However, in the case where a developer may need to retain a small chunk of +memory from a pool for an indeterminate amount of time, it may be appropriate +to create an un-pooled Buffer instance using `Buffer.allocUnsafeSlow()` then +copy out the relevant bits. + +```js +// need to keep around a few small chunks of memory +const store = []; + +socket.on('readable', () => { + const data = socket.read(); + // allocate for retained data + const sb = Buffer.allocUnsafeSlow(10); + // copy the data into the new allocation + data.copy(sb, 0, 0, 10); + store.push(sb); +}); +``` + +Use of `Buffer.allocUnsafeSlow()` should be used only as a last resort *after* +a developer has observed undue memory retention in their applications. + +A `TypeError` will be thrown if `size` is not a number. + +### All the Rest + +The rest of the `Buffer` API is exactly the same as in node.js. +[See the docs](https://nodejs.org/api/buffer.html). + + +## Related links + +- [Node.js issue: Buffer(number) is unsafe](https://github.com/nodejs/node/issues/4660) +- [Node.js Enhancement Proposal: Buffer.from/Buffer.alloc/Buffer.zalloc/Buffer() soft-deprecate](https://github.com/nodejs/node-eps/pull/4) + +## Why is `Buffer` unsafe? + +Today, the node.js `Buffer` constructor is overloaded to handle many different argument +types like `String`, `Array`, `Object`, `TypedArrayView` (`Uint8Array`, etc.), +`ArrayBuffer`, and also `Number`. + +The API is optimized for convenience: you can throw any type at it, and it will try to do +what you want. + +Because the Buffer constructor is so powerful, you often see code like this: + +```js +// Convert UTF-8 strings to hex +function toHex (str) { + return new Buffer(str).toString('hex') +} +``` + +***But what happens if `toHex` is called with a `Number` argument?*** + +### Remote Memory Disclosure + +If an attacker can make your program call the `Buffer` constructor with a `Number` +argument, then they can make it allocate uninitialized memory from the node.js process. +This could potentially disclose TLS private keys, user data, or database passwords. + +When the `Buffer` constructor is passed a `Number` argument, it returns an +**UNINITIALIZED** block of memory of the specified `size`. When you create a `Buffer` like +this, you **MUST** overwrite the contents before returning it to the user. + +From the [node.js docs](https://nodejs.org/api/buffer.html#buffer_new_buffer_size): + +> `new Buffer(size)` +> +> - `size` Number +> +> The underlying memory for `Buffer` instances created in this way is not initialized. +> **The contents of a newly created `Buffer` are unknown and could contain sensitive +> data.** Use `buf.fill(0)` to initialize a Buffer to zeroes. + +(Emphasis our own.) + +Whenever the programmer intended to create an uninitialized `Buffer` you often see code +like this: + +```js +var buf = new Buffer(16) + +// Immediately overwrite the uninitialized buffer with data from another buffer +for (var i = 0; i < buf.length; i++) { + buf[i] = otherBuf[i] +} +``` + + +### Would this ever be a problem in real code? + +Yes. It's surprisingly common to forget to check the type of your variables in a +dynamically-typed language like JavaScript. + +Usually the consequences of assuming the wrong type is that your program crashes with an +uncaught exception. But the failure mode for forgetting to check the type of arguments to +the `Buffer` constructor is more catastrophic. + +Here's an example of a vulnerable service that takes a JSON payload and converts it to +hex: + +```js +// Take a JSON payload {str: "some string"} and convert it to hex +var server = http.createServer(function (req, res) { + var data = '' + req.setEncoding('utf8') + req.on('data', function (chunk) { + data += chunk + }) + req.on('end', function () { + var body = JSON.parse(data) + res.end(new Buffer(body.str).toString('hex')) + }) +}) + +server.listen(8080) +``` + +In this example, an http client just has to send: + +```json +{ + "str": 1000 +} +``` + +and it will get back 1,000 bytes of uninitialized memory from the server. + +This is a very serious bug. It's similar in severity to the +[the Heartbleed bug](http://heartbleed.com/) that allowed disclosure of OpenSSL process +memory by remote attackers. + + +### Which real-world packages were vulnerable? + +#### [`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht) + +[Mathias Buus](https://github.com/mafintosh) and I +([Feross Aboukhadijeh](http://feross.org/)) found this issue in one of our own packages, +[`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht). The bug would allow +anyone on the internet to send a series of messages to a user of `bittorrent-dht` and get +them to reveal 20 bytes at a time of uninitialized memory from the node.js process. + +Here's +[the commit](https://github.com/feross/bittorrent-dht/commit/6c7da04025d5633699800a99ec3fbadf70ad35b8) +that fixed it. We released a new fixed version, created a +[Node Security Project disclosure](https://nodesecurity.io/advisories/68), and deprecated all +vulnerable versions on npm so users will get a warning to upgrade to a newer version. + +#### [`ws`](https://www.npmjs.com/package/ws) + +That got us wondering if there were other vulnerable packages. Sure enough, within a short +period of time, we found the same issue in [`ws`](https://www.npmjs.com/package/ws), the +most popular WebSocket implementation in node.js. + +If certain APIs were called with `Number` parameters instead of `String` or `Buffer` as +expected, then uninitialized server memory would be disclosed to the remote peer. + +These were the vulnerable methods: + +```js +socket.send(number) +socket.ping(number) +socket.pong(number) +``` + +Here's a vulnerable socket server with some echo functionality: + +```js +server.on('connection', function (socket) { + socket.on('message', function (message) { + message = JSON.parse(message) + if (message.type === 'echo') { + socket.send(message.data) // send back the user's message + } + }) +}) +``` + +`socket.send(number)` called on the server, will disclose server memory. + +Here's [the release](https://github.com/websockets/ws/releases/tag/1.0.1) where the issue +was fixed, with a more detailed explanation. Props to +[Arnout Kazemier](https://github.com/3rd-Eden) for the quick fix. Here's the +[Node Security Project disclosure](https://nodesecurity.io/advisories/67). + + +### What's the solution? + +It's important that node.js offers a fast way to get memory otherwise performance-critical +applications would needlessly get a lot slower. + +But we need a better way to *signal our intent* as programmers. **When we want +uninitialized memory, we should request it explicitly.** + +Sensitive functionality should not be packed into a developer-friendly API that loosely +accepts many different types. This type of API encourages the lazy practice of passing +variables in without checking the type very carefully. + +#### A new API: `Buffer.allocUnsafe(number)` + +The functionality of creating buffers with uninitialized memory should be part of another +API. We propose `Buffer.allocUnsafe(number)`. This way, it's not part of an API that +frequently gets user input of all sorts of different types passed into it. + +```js +var buf = Buffer.allocUnsafe(16) // careful, uninitialized memory! + +// Immediately overwrite the uninitialized buffer with data from another buffer +for (var i = 0; i < buf.length; i++) { + buf[i] = otherBuf[i] +} +``` + + +### How do we fix node.js core? + +We sent [a PR to node.js core](https://github.com/nodejs/node/pull/4514) (merged as +`semver-major`) which defends against one case: + +```js +var str = 16 +new Buffer(str, 'utf8') +``` + +In this situation, it's implied that the programmer intended the first argument to be a +string, since they passed an encoding as a second argument. Today, node.js will allocate +uninitialized memory in the case of `new Buffer(number, encoding)`, which is probably not +what the programmer intended. + +But this is only a partial solution, since if the programmer does `new Buffer(variable)` +(without an `encoding` parameter) there's no way to know what they intended. If `variable` +is sometimes a number, then uninitialized memory will sometimes be returned. + +### What's the real long-term fix? + +We could deprecate and remove `new Buffer(number)` and use `Buffer.allocUnsafe(number)` when +we need uninitialized memory. But that would break 1000s of packages. + +~~We believe the best solution is to:~~ + +~~1. Change `new Buffer(number)` to return safe, zeroed-out memory~~ + +~~2. Create a new API for creating uninitialized Buffers. We propose: `Buffer.allocUnsafe(number)`~~ + +#### Update + +We now support adding three new APIs: + +- `Buffer.from(value)` - convert from any type to a buffer +- `Buffer.alloc(size)` - create a zero-filled buffer +- `Buffer.allocUnsafe(size)` - create an uninitialized buffer with given size + +This solves the core problem that affected `ws` and `bittorrent-dht` which is +`Buffer(variable)` getting tricked into taking a number argument. + +This way, existing code continues working and the impact on the npm ecosystem will be +minimal. Over time, npm maintainers can migrate performance-critical code to use +`Buffer.allocUnsafe(number)` instead of `new Buffer(number)`. + + +### Conclusion + +We think there's a serious design issue with the `Buffer` API as it exists today. It +promotes insecure software by putting high-risk functionality into a convenient API +with friendly "developer ergonomics". + +This wasn't merely a theoretical exercise because we found the issue in some of the +most popular npm packages. + +Fortunately, there's an easy fix that can be applied today. Use `safe-buffer` in place of +`buffer`. + +```js +var Buffer = require('safe-buffer').Buffer +``` + +Eventually, we hope that node.js core can switch to this new, safer behavior. We believe +the impact on the ecosystem would be minimal since it's not a breaking change. +Well-maintained, popular packages would be updated to use `Buffer.alloc` quickly, while +older, insecure packages would magically become safe from this attack vector. + + +## links + +- [Node.js PR: buffer: throw if both length and enc are passed](https://github.com/nodejs/node/pull/4514) +- [Node Security Project disclosure for `ws`](https://nodesecurity.io/advisories/67) +- [Node Security Project disclosure for`bittorrent-dht`](https://nodesecurity.io/advisories/68) + + +## credit + +The original issues in `bittorrent-dht` +([disclosure](https://nodesecurity.io/advisories/68)) and +`ws` ([disclosure](https://nodesecurity.io/advisories/67)) were discovered by +[Mathias Buus](https://github.com/mafintosh) and +[Feross Aboukhadijeh](http://feross.org/). + +Thanks to [Adam Baldwin](https://github.com/evilpacket) for helping disclose these issues +and for his work running the [Node Security Project](https://nodesecurity.io/). + +Thanks to [John Hiesey](https://github.com/jhiesey) for proofreading this README and +auditing the code. + + +## license + +MIT. Copyright (C) [Feross Aboukhadijeh](http://feross.org) diff --git a/node_modules/ws/node_modules/safe-buffer/browser.js b/node_modules/ws/node_modules/safe-buffer/browser.js new file mode 100644 index 0000000..0bd1202 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/browser.js @@ -0,0 +1 @@ +module.exports = require('buffer') diff --git a/node_modules/ws/node_modules/safe-buffer/index.js b/node_modules/ws/node_modules/safe-buffer/index.js new file mode 100644 index 0000000..74a7358 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/index.js @@ -0,0 +1,58 @@ +var buffer = require('buffer') + +if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { + module.exports = buffer +} else { + // Copy properties from require('buffer') + Object.keys(buffer).forEach(function (prop) { + exports[prop] = buffer[prop] + }) + exports.Buffer = SafeBuffer +} + +function SafeBuffer (arg, encodingOrOffset, length) { + return Buffer(arg, encodingOrOffset, length) +} + +// Copy static methods from Buffer +Object.keys(Buffer).forEach(function (prop) { + SafeBuffer[prop] = Buffer[prop] +}) + +SafeBuffer.from = function (arg, encodingOrOffset, length) { + if (typeof arg === 'number') { + throw new TypeError('Argument must not be a number') + } + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + var buf = Buffer(size) + if (fill !== undefined) { + if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + } else { + buf.fill(0) + } + return buf +} + +SafeBuffer.allocUnsafe = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return Buffer(size) +} + +SafeBuffer.allocUnsafeSlow = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return buffer.SlowBuffer(size) +} diff --git a/node_modules/ws/node_modules/safe-buffer/package.json b/node_modules/ws/node_modules/safe-buffer/package.json new file mode 100644 index 0000000..fcb6641 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/package.json @@ -0,0 +1,103 @@ +{ + "_args": [ + [ + { + "raw": "safe-buffer@~5.0.1", + "scope": null, + "escapedName": "safe-buffer", + "name": "safe-buffer", + "rawSpec": "~5.0.1", + "spec": ">=5.0.1 <5.1.0", + "type": "range" + }, + "/mnt/e/Yaroslav/Documents/Webs/nodejs/checkers/node_modules/ws" + ] + ], + "_from": "safe-buffer@>=5.0.1 <5.1.0", + "_id": "safe-buffer@5.0.1", + "_inCache": true, + "_location": "/ws/safe-buffer", + "_nodeVersion": "4.4.5", + "_npmOperationalInternal": { + "host": "packages-12-west.internal.npmjs.com", + "tmp": "tmp/safe-buffer-5.0.1.tgz_1464588482081_0.8112505874596536" + }, + "_npmUser": { + "name": "feross", + "email": "feross@feross.org" + }, + "_npmVersion": "2.15.5", + "_phantomChildren": {}, + "_requested": { + "raw": "safe-buffer@~5.0.1", + "scope": null, + "escapedName": "safe-buffer", + "name": "safe-buffer", + "rawSpec": "~5.0.1", + "spec": ">=5.0.1 <5.1.0", + "type": "range" + }, + "_requiredBy": [ + "/ws" + ], + "_resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "_shasum": "d263ca54696cd8a306b5ca6551e92de57918fbe7", + "_shrinkwrap": null, + "_spec": "safe-buffer@~5.0.1", + "_where": "/mnt/e/Yaroslav/Documents/Webs/nodejs/checkers/node_modules/ws", + "author": { + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "http://feross.org" + }, + "browser": "./browser.js", + "bugs": { + "url": "https://github.com/feross/safe-buffer/issues" + }, + "dependencies": {}, + "description": "Safer Node.js Buffer API", + "devDependencies": { + "standard": "^7.0.0", + "tape": "^4.0.0", + "zuul": "^3.0.0" + }, + "directories": {}, + "dist": { + "shasum": "d263ca54696cd8a306b5ca6551e92de57918fbe7", + "tarball": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz" + }, + "gitHead": "1e371a367da962afae2bebc527b50271c739d28c", + "homepage": "https://github.com/feross/safe-buffer", + "keywords": [ + "buffer", + "buffer allocate", + "node security", + "safe", + "safe-buffer", + "security", + "uninitialized" + ], + "license": "MIT", + "main": "index.js", + "maintainers": [ + { + "name": "feross", + "email": "feross@feross.org" + }, + { + "name": "mafintosh", + "email": "mathiasbuus@gmail.com" + } + ], + "name": "safe-buffer", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git://github.com/feross/safe-buffer.git" + }, + "scripts": { + "test": "standard && tape test.js" + }, + "version": "5.0.1" +} diff --git a/node_modules/ws/node_modules/safe-buffer/test.js b/node_modules/ws/node_modules/safe-buffer/test.js new file mode 100644 index 0000000..7da8ad7 --- /dev/null +++ b/node_modules/ws/node_modules/safe-buffer/test.js @@ -0,0 +1,99 @@ +var test = require('tape') +var SafeBuffer = require('./').Buffer + +test('new SafeBuffer(value) works just like Buffer', function (t) { + t.deepEqual(new SafeBuffer('hey'), new Buffer('hey')) + t.deepEqual(new SafeBuffer('hey', 'utf8'), new Buffer('hey', 'utf8')) + t.deepEqual(new SafeBuffer('686579', 'hex'), new Buffer('686579', 'hex')) + t.deepEqual(new SafeBuffer([1, 2, 3]), new Buffer([1, 2, 3])) + t.deepEqual(new SafeBuffer(new Uint8Array([1, 2, 3])), new Buffer(new Uint8Array([1, 2, 3]))) + + t.equal(typeof SafeBuffer.isBuffer, 'function') + t.equal(SafeBuffer.isBuffer(new SafeBuffer('hey')), true) + t.equal(Buffer.isBuffer(new SafeBuffer('hey')), true) + t.notOk(SafeBuffer.isBuffer({})) + + t.end() +}) + +test('SafeBuffer.from(value) converts to a Buffer', function (t) { + t.deepEqual(SafeBuffer.from('hey'), new Buffer('hey')) + t.deepEqual(SafeBuffer.from('hey', 'utf8'), new Buffer('hey', 'utf8')) + t.deepEqual(SafeBuffer.from('686579', 'hex'), new Buffer('686579', 'hex')) + t.deepEqual(SafeBuffer.from([1, 2, 3]), new Buffer([1, 2, 3])) + t.deepEqual(SafeBuffer.from(new Uint8Array([1, 2, 3])), new Buffer(new Uint8Array([1, 2, 3]))) + + t.end() +}) + +test('SafeBuffer.alloc(number) returns zeroed-out memory', function (t) { + for (var i = 0; i < 10; i++) { + var expected1 = new Buffer(1000) + expected1.fill(0) + t.deepEqual(SafeBuffer.alloc(1000), expected1) + + var expected2 = new Buffer(1000 * 1000) + expected2.fill(0) + t.deepEqual(SafeBuffer.alloc(1000 * 1000), expected2) + } + t.end() +}) + +test('SafeBuffer.allocUnsafe(number)', function (t) { + var buf = SafeBuffer.allocUnsafe(100) // unitialized memory + t.equal(buf.length, 100) + t.equal(SafeBuffer.isBuffer(buf), true) + t.equal(Buffer.isBuffer(buf), true) + t.end() +}) + +test('SafeBuffer.from() throws with number types', function (t) { + t.plan(5) + t.throws(function () { + SafeBuffer.from(0) + }) + t.throws(function () { + SafeBuffer.from(-1) + }) + t.throws(function () { + SafeBuffer.from(NaN) + }) + t.throws(function () { + SafeBuffer.from(Infinity) + }) + t.throws(function () { + SafeBuffer.from(99) + }) +}) + +test('SafeBuffer.allocUnsafe() throws with non-number types', function (t) { + t.plan(4) + t.throws(function () { + SafeBuffer.allocUnsafe('hey') + }) + t.throws(function () { + SafeBuffer.allocUnsafe('hey', 'utf8') + }) + t.throws(function () { + SafeBuffer.allocUnsafe([1, 2, 3]) + }) + t.throws(function () { + SafeBuffer.allocUnsafe({}) + }) +}) + +test('SafeBuffer.alloc() throws with non-number types', function (t) { + t.plan(4) + t.throws(function () { + SafeBuffer.alloc('hey') + }) + t.throws(function () { + SafeBuffer.alloc('hey', 'utf8') + }) + t.throws(function () { + SafeBuffer.alloc([1, 2, 3]) + }) + t.throws(function () { + SafeBuffer.alloc({}) + }) +}) diff --git a/node_modules/ws/package.json b/node_modules/ws/package.json new file mode 100644 index 0000000..d007539 --- /dev/null +++ b/node_modules/ws/package.json @@ -0,0 +1,127 @@ +{ + "_args": [ + [ + { + "raw": "ws@~2.3.1", + "scope": null, + "escapedName": "ws", + "name": "ws", + "rawSpec": "~2.3.1", + "spec": ">=2.3.1 <2.4.0", + "type": "range" + }, + "/mnt/e/Yaroslav/Documents/Webs/nodejs/checkers/node_modules/engine.io" + ] + ], + "_from": "ws@>=2.3.1 <2.4.0", + "_id": "ws@2.3.1", + "_inCache": true, + "_location": "/ws", + "_nodeVersion": "7.9.0", + "_npmOperationalInternal": { + "host": "packages-12-west.internal.npmjs.com", + "tmp": "tmp/ws-2.3.1.tgz_1492711201097_0.04034068179316819" + }, + "_npmUser": { + "name": "lpinca", + "email": "luigipinca@gmail.com" + }, + "_npmVersion": "4.2.0", + "_phantomChildren": {}, + "_requested": { + "raw": "ws@~2.3.1", + "scope": null, + "escapedName": "ws", + "name": "ws", + "rawSpec": "~2.3.1", + "spec": ">=2.3.1 <2.4.0", + "type": "range" + }, + "_requiredBy": [ + "/engine.io", + "/engine.io-client" + ], + "_resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz", + "_shasum": "6b94b3e447cb6a363f785eaf94af6359e8e81c80", + "_shrinkwrap": null, + "_spec": "ws@~2.3.1", + "_where": "/mnt/e/Yaroslav/Documents/Webs/nodejs/checkers/node_modules/engine.io", + "author": { + "name": "Einar Otto Stangvik", + "email": "einaros@gmail.com", + "url": "http://2x.io" + }, + "bugs": { + "url": "https://github.com/websockets/ws/issues" + }, + "dependencies": { + "safe-buffer": "~5.0.1", + "ultron": "~1.1.0" + }, + "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", + "devDependencies": { + "benchmark": "~2.1.2", + "bufferutil": "~3.0.0", + "eslint": "~3.19.0", + "eslint-config-standard": "~10.2.0", + "eslint-plugin-import": "~2.2.0", + "eslint-plugin-node": "~4.2.0", + "eslint-plugin-promise": "~3.5.0", + "eslint-plugin-standard": "~3.0.0", + "mocha": "~3.2.0", + "nyc": "~10.2.0", + "utf-8-validate": "~3.0.0" + }, + "directories": {}, + "dist": { + "shasum": "6b94b3e447cb6a363f785eaf94af6359e8e81c80", + "tarball": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz" + }, + "files": [ + "index.js", + "lib" + ], + "gitHead": "732aaf06b76700f104eeff2740e1896be4e88199", + "homepage": "https://github.com/websockets/ws", + "keywords": [ + "HyBi", + "Push", + "RFC-6455", + "WebSocket", + "WebSockets", + "real-time" + ], + "license": "MIT", + "main": "index.js", + "maintainers": [ + { + "name": "3rdeden", + "email": "npm@3rd-Eden.com" + }, + { + "name": "einaros", + "email": "einaros@gmail.com" + }, + { + "name": "lpinca", + "email": "luigipinca@gmail.com" + }, + { + "name": "v1", + "email": "npm@3rd-Eden.com" + } + ], + "name": "ws", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git+https://github.com/websockets/ws.git" + }, + "scripts": { + "integration": "eslint . && mocha test/*.integration.js", + "lint": "eslint .", + "test": "eslint . && nyc --reporter=html --reporter=text mocha test/*.test.js" + }, + "version": "2.3.1" +} |