mqtt stuff added
This commit is contained in:
BIN
node_modules/ws/lib/.DS_Store
generated
vendored
Normal file
BIN
node_modules/ws/lib/.DS_Store
generated
vendored
Normal file
Binary file not shown.
71
node_modules/ws/lib/BufferUtil.js
generated
vendored
Normal file
71
node_modules/ws/lib/BufferUtil.js
generated
vendored
Normal file
@ -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 };
|
||||
}
|
10
node_modules/ws/lib/Constants.js
generated
vendored
Normal file
10
node_modules/ws/lib/Constants.js
generated
vendored
Normal file
@ -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 = () => {};
|
28
node_modules/ws/lib/ErrorCodes.js
generated
vendored
Normal file
28
node_modules/ws/lib/ErrorCodes.js
generated
vendored
Normal file
@ -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'
|
||||
};
|
151
node_modules/ws/lib/EventTarget.js
generated
vendored
Normal file
151
node_modules/ws/lib/EventTarget.js
generated
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Class representing an event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class Event {
|
||||
/**
|
||||
* Create a new `Event`.
|
||||
*
|
||||
* @param {String} type The name of the event
|
||||
* @param {Object} target A reference to the target to which the event was dispatched
|
||||
*/
|
||||
constructor (type, target) {
|
||||
this.target = target;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a message event.
|
||||
*
|
||||
* @extends Event
|
||||
* @private
|
||||
*/
|
||||
class MessageEvent extends Event {
|
||||
/**
|
||||
* Create a new `MessageEvent`.
|
||||
*
|
||||
* @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data
|
||||
* @param {WebSocket} target A reference to the target to which the event was dispatched
|
||||
*/
|
||||
constructor (data, target) {
|
||||
super('message', target);
|
||||
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a close event.
|
||||
*
|
||||
* @extends Event
|
||||
* @private
|
||||
*/
|
||||
class CloseEvent extends Event {
|
||||
/**
|
||||
* Create a new `CloseEvent`.
|
||||
*
|
||||
* @param {Number} code The status code explaining why the connection is being closed
|
||||
* @param {String} reason A human-readable string explaining why the connection is closing
|
||||
* @param {WebSocket} target A reference to the target to which the event was dispatched
|
||||
*/
|
||||
constructor (code, reason, target) {
|
||||
super('close', target);
|
||||
|
||||
this.wasClean = target._closeFrameReceived && target._closeFrameSent;
|
||||
this.reason = reason;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing an open event.
|
||||
*
|
||||
* @extends Event
|
||||
* @private
|
||||
*/
|
||||
class OpenEvent extends Event {
|
||||
/**
|
||||
* Create a new `OpenEvent`.
|
||||
*
|
||||
* @param {WebSocket} target A reference to the target to which the event was dispatched
|
||||
*/
|
||||
constructor (target) {
|
||||
super('open', target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
listener.call(this, new MessageEvent(data, 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;
|
203
node_modules/ws/lib/Extensions.js
generated
vendored
Normal file
203
node_modules/ws/lib/Extensions.js
generated
vendored
Normal file
@ -0,0 +1,203 @@
|
||||
'use strict';
|
||||
|
||||
//
|
||||
// Allowed token characters:
|
||||
//
|
||||
// '!', '#', '$', '%', '&', ''', '*', '+', '-',
|
||||
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
|
||||
//
|
||||
// tokenChars[32] === 0 // ' '
|
||||
// tokenChars[33] === 1 // '!'
|
||||
// tokenChars[34] === 0 // '"'
|
||||
// ...
|
||||
//
|
||||
const tokenChars = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
|
||||
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
|
||||
];
|
||||
|
||||
/**
|
||||
* Adds an offer to the map of extension offers or a parameter to the map of
|
||||
* parameters.
|
||||
*
|
||||
* @param {Object} dest The map of extension offers or parameters
|
||||
* @param {String} name The extension or parameter name
|
||||
* @param {(Object|Boolean|String)} elem The extension parameters or the
|
||||
* parameter value
|
||||
* @private
|
||||
*/
|
||||
function push (dest, name, elem) {
|
||||
if (Object.prototype.hasOwnProperty.call(dest, name)) dest[name].push(elem);
|
||||
else dest[name] = [elem];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the `Sec-WebSocket-Extensions` header into an object.
|
||||
*
|
||||
* @param {String} header The field value of the header
|
||||
* @return {Object} The parsed object
|
||||
* @public
|
||||
*/
|
||||
function parse (header) {
|
||||
const offers = {};
|
||||
|
||||
if (header === undefined || header === '') return offers;
|
||||
|
||||
var params = {};
|
||||
var mustUnescape = false;
|
||||
var isEscaping = false;
|
||||
var inQuotes = false;
|
||||
var extensionName;
|
||||
var paramName;
|
||||
var start = -1;
|
||||
var end = -1;
|
||||
|
||||
for (var i = 0; i < header.length; i++) {
|
||||
const code = header.charCodeAt(i);
|
||||
|
||||
if (extensionName === undefined) {
|
||||
if (end === -1 && tokenChars[code] === 1) {
|
||||
if (start === -1) start = i;
|
||||
} else if (code === 0x20/* ' ' */|| code === 0x09/* '\t' */) {
|
||||
if (end === -1 && start !== -1) end = i;
|
||||
} else if (code === 0x3b/* ';' */ || code === 0x2c/* ',' */) {
|
||||
if (start === -1) throw new Error(`unexpected character at index ${i}`);
|
||||
|
||||
if (end === -1) end = i;
|
||||
const name = header.slice(start, end);
|
||||
if (code === 0x2c) {
|
||||
push(offers, name, params);
|
||||
params = {};
|
||||
} else {
|
||||
extensionName = name;
|
||||
}
|
||||
|
||||
start = end = -1;
|
||||
} else {
|
||||
throw new Error(`unexpected character at index ${i}`);
|
||||
}
|
||||
} else if (paramName === undefined) {
|
||||
if (end === -1 && tokenChars[code] === 1) {
|
||||
if (start === -1) start = i;
|
||||
} else if (code === 0x20 || code === 0x09) {
|
||||
if (end === -1 && start !== -1) end = i;
|
||||
} else if (code === 0x3b || code === 0x2c) {
|
||||
if (start === -1) throw new Error(`unexpected character at index ${i}`);
|
||||
|
||||
if (end === -1) end = i;
|
||||
push(params, header.slice(start, end), true);
|
||||
if (code === 0x2c) {
|
||||
push(offers, extensionName, params);
|
||||
params = {};
|
||||
extensionName = undefined;
|
||||
}
|
||||
|
||||
start = end = -1;
|
||||
} else if (code === 0x3d/* '=' */&& start !== -1 && end === -1) {
|
||||
paramName = header.slice(start, i);
|
||||
start = end = -1;
|
||||
} else {
|
||||
throw new Error(`unexpected character at index ${i}`);
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// The value of a quoted-string after unescaping must conform to the
|
||||
// token ABNF, so only token characters are valid.
|
||||
// Ref: https://tools.ietf.org/html/rfc6455#section-9.1
|
||||
//
|
||||
if (isEscaping) {
|
||||
if (tokenChars[code] !== 1) {
|
||||
throw new Error(`unexpected character at index ${i}`);
|
||||
}
|
||||
if (start === -1) start = i;
|
||||
else if (!mustUnescape) mustUnescape = true;
|
||||
isEscaping = false;
|
||||
} else if (inQuotes) {
|
||||
if (tokenChars[code] === 1) {
|
||||
if (start === -1) start = i;
|
||||
} else if (code === 0x22/* '"' */ && start !== -1) {
|
||||
inQuotes = false;
|
||||
end = i;
|
||||
} else if (code === 0x5c/* '\' */) {
|
||||
isEscaping = true;
|
||||
} else {
|
||||
throw new Error(`unexpected character at index ${i}`);
|
||||
}
|
||||
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
|
||||
inQuotes = true;
|
||||
} else if (end === -1 && tokenChars[code] === 1) {
|
||||
if (start === -1) start = i;
|
||||
} else if (start !== -1 && (code === 0x20 || code === 0x09)) {
|
||||
if (end === -1) end = i;
|
||||
} else if (code === 0x3b || code === 0x2c) {
|
||||
if (start === -1) throw new Error(`unexpected character at index ${i}`);
|
||||
|
||||
if (end === -1) end = i;
|
||||
var value = header.slice(start, end);
|
||||
if (mustUnescape) {
|
||||
value = value.replace(/\\/g, '');
|
||||
mustUnescape = false;
|
||||
}
|
||||
push(params, paramName, value);
|
||||
if (code === 0x2c) {
|
||||
push(offers, extensionName, params);
|
||||
params = {};
|
||||
extensionName = undefined;
|
||||
}
|
||||
|
||||
paramName = undefined;
|
||||
start = end = -1;
|
||||
} else {
|
||||
throw new Error(`unexpected character at index ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (start === -1 || inQuotes) throw new Error('unexpected end of input');
|
||||
|
||||
if (end === -1) end = i;
|
||||
const token = header.slice(start, end);
|
||||
if (extensionName === undefined) {
|
||||
push(offers, token, {});
|
||||
} else {
|
||||
if (paramName === undefined) {
|
||||
push(params, token, true);
|
||||
} else if (mustUnescape) {
|
||||
push(params, paramName, token.replace(/\\/g, ''));
|
||||
} else {
|
||||
push(params, paramName, token);
|
||||
}
|
||||
push(offers, extensionName, params);
|
||||
}
|
||||
|
||||
return offers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes 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
|
||||
*/
|
||||
function 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 };
|
507
node_modules/ws/lib/PerMessageDeflate.js
generated
vendored
Normal file
507
node_modules/ws/lib/PerMessageDeflate.js
generated
vendored
Normal file
@ -0,0 +1,507 @@
|
||||
'use strict';
|
||||
|
||||
const safeBuffer = require('safe-buffer');
|
||||
const Limiter = require('async-limiter');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const bufferUtil = require('./BufferUtil');
|
||||
|
||||
const Buffer = safeBuffer.Buffer;
|
||||
|
||||
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
|
||||
const EMPTY_BLOCK = Buffer.from([0x00]);
|
||||
|
||||
const kWriteInProgress = Symbol('write-in-progress');
|
||||
const kPendingClose = Symbol('pending-close');
|
||||
const kTotalLength = Symbol('total-length');
|
||||
const kCallback = Symbol('callback');
|
||||
const kBuffers = Symbol('buffers');
|
||||
const kError = Symbol('error');
|
||||
const kOwner = Symbol('owner');
|
||||
|
||||
//
|
||||
// We limit zlib concurrency, which prevents severe memory fragmentation
|
||||
// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
|
||||
// and https://github.com/websockets/ws/issues/1202
|
||||
//
|
||||
// Intentionally global; it's the global thread pool that's an issue.
|
||||
//
|
||||
let zlibLimiter;
|
||||
|
||||
/**
|
||||
* permessage-deflate implementation.
|
||||
*/
|
||||
class PerMessageDeflate {
|
||||
/**
|
||||
* Creates a PerMessageDeflate instance.
|
||||
*
|
||||
* @param {Object} options Configuration options
|
||||
* @param {Boolean} options.serverNoContextTakeover Request/accept disabling
|
||||
* of server context takeover
|
||||
* @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge
|
||||
* disabling of client context takeover
|
||||
* @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the
|
||||
* use of a custom server window size
|
||||
* @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support
|
||||
* for, or request, a custom client window size
|
||||
* @param {Number} options.level The value of zlib's `level` param
|
||||
* @param {Number} options.memLevel The value of zlib's `memLevel` param
|
||||
* @param {Number} options.threshold Size (in bytes) below which messages
|
||||
* should not be compressed
|
||||
* @param {Number} options.concurrencyLimit The number of concurrent calls to
|
||||
* zlib
|
||||
* @param {Boolean} isServer Create the instance in either server or client
|
||||
* mode
|
||||
* @param {Number} maxPayload The maximum allowed message length
|
||||
*/
|
||||
constructor (options, isServer, maxPayload) {
|
||||
this._maxPayload = maxPayload | 0;
|
||||
this._options = options || {};
|
||||
this._threshold = this._options.threshold !== undefined
|
||||
? this._options.threshold
|
||||
: 1024;
|
||||
this._isServer = !!isServer;
|
||||
this._deflate = null;
|
||||
this._inflate = null;
|
||||
|
||||
this.params = null;
|
||||
|
||||
if (!zlibLimiter) {
|
||||
const concurrency = this._options.concurrencyLimit !== undefined
|
||||
? this._options.concurrencyLimit
|
||||
: 10;
|
||||
zlibLimiter = new Limiter({ concurrency });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
static get extensionName () {
|
||||
return 'permessage-deflate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create 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[kWriteInProgress]) {
|
||||
this._inflate[kPendingClose] = true;
|
||||
} else {
|
||||
this._inflate.close();
|
||||
this._inflate = null;
|
||||
}
|
||||
}
|
||||
if (this._deflate) {
|
||||
if (this._deflate[kWriteInProgress]) {
|
||||
this._deflate[kPendingClose] = 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 ||
|
||||
(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 === false &&
|
||||
params.client_no_context_takeover
|
||||
) {
|
||||
throw new Error('Invalid value for "client_no_context_takeover"');
|
||||
}
|
||||
|
||||
if (
|
||||
(typeof this._options.clientMaxWindowBits === 'number' &&
|
||||
(!params.client_max_window_bits ||
|
||||
params.client_max_window_bits > this._options.clientMaxWindowBits)) ||
|
||||
(this._options.clientMaxWindowBits === false &&
|
||||
params.client_max_window_bits)
|
||||
) {
|
||||
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 (
|
||||
Number.isNaN(value) ||
|
||||
value < zlib.Z_MIN_WINDOWBITS ||
|
||||
value > zlib.Z_MAX_WINDOWBITS
|
||||
) {
|
||||
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. Concurrency limited by async-limiter.
|
||||
*
|
||||
* @param {Buffer} data Compressed data
|
||||
* @param {Boolean} fin Specifies whether or not this is the last fragment
|
||||
* @param {Function} callback Callback
|
||||
* @public
|
||||
*/
|
||||
decompress (data, fin, callback) {
|
||||
zlibLimiter.push((done) => {
|
||||
this._decompress(data, fin, (err, result) => {
|
||||
done();
|
||||
callback(err, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress data. Concurrency limited by async-limiter.
|
||||
*
|
||||
* @param {Buffer} data Data to compress
|
||||
* @param {Boolean} fin Specifies whether or not this is the last fragment
|
||||
* @param {Function} callback Callback
|
||||
* @public
|
||||
*/
|
||||
compress (data, fin, callback) {
|
||||
zlibLimiter.push((done) => {
|
||||
this._compress(data, fin, (err, result) => {
|
||||
done();
|
||||
callback(err, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress data.
|
||||
*
|
||||
* @param {Buffer} data Compressed data
|
||||
* @param {Boolean} fin Specifies whether or not this is the last fragment
|
||||
* @param {Function} callback Callback
|
||||
* @private
|
||||
*/
|
||||
_decompress (data, fin, callback) {
|
||||
const endpoint = this._isServer ? 'client' : 'server';
|
||||
|
||||
if (!this._inflate) {
|
||||
const key = `${endpoint}_max_window_bits`;
|
||||
const windowBits = typeof this.params[key] !== 'number'
|
||||
? zlib.Z_DEFAULT_WINDOWBITS
|
||||
: this.params[key];
|
||||
|
||||
this._inflate = zlib.createInflateRaw({ windowBits });
|
||||
this._inflate[kTotalLength] = 0;
|
||||
this._inflate[kBuffers] = [];
|
||||
this._inflate[kOwner] = this;
|
||||
this._inflate.on('error', inflateOnError);
|
||||
this._inflate.on('data', inflateOnData);
|
||||
}
|
||||
|
||||
this._inflate[kCallback] = callback;
|
||||
this._inflate[kWriteInProgress] = true;
|
||||
|
||||
this._inflate.write(data);
|
||||
if (fin) this._inflate.write(TRAILER);
|
||||
|
||||
this._inflate.flush(() => {
|
||||
const err = this._inflate[kError];
|
||||
|
||||
if (err) {
|
||||
this._inflate.close();
|
||||
this._inflate = null;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = bufferUtil.concat(
|
||||
this._inflate[kBuffers],
|
||||
this._inflate[kTotalLength]
|
||||
);
|
||||
|
||||
if (
|
||||
(fin && this.params[`${endpoint}_no_context_takeover`]) ||
|
||||
this._inflate[kPendingClose]
|
||||
) {
|
||||
this._inflate.close();
|
||||
this._inflate = null;
|
||||
} else {
|
||||
this._inflate[kWriteInProgress] = false;
|
||||
this._inflate[kTotalLength] = 0;
|
||||
this._inflate[kBuffers] = [];
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress data.
|
||||
*
|
||||
* @param {Buffer} data Data to compress
|
||||
* @param {Boolean} fin Specifies whether or not this is the last fragment
|
||||
* @param {Function} callback Callback
|
||||
* @private
|
||||
*/
|
||||
_compress (data, fin, callback) {
|
||||
if (!data || data.length === 0) {
|
||||
process.nextTick(callback, null, EMPTY_BLOCK);
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = this._isServer ? 'server' : 'client';
|
||||
|
||||
if (!this._deflate) {
|
||||
const key = `${endpoint}_max_window_bits`;
|
||||
const windowBits = typeof this.params[key] !== 'number'
|
||||
? zlib.Z_DEFAULT_WINDOWBITS
|
||||
: this.params[key];
|
||||
|
||||
this._deflate = zlib.createDeflateRaw({
|
||||
memLevel: this._options.memLevel,
|
||||
level: this._options.level,
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
windowBits
|
||||
});
|
||||
|
||||
this._deflate[kTotalLength] = 0;
|
||||
this._deflate[kBuffers] = [];
|
||||
|
||||
//
|
||||
// `zlib.DeflateRaw` emits an `'error'` event only when an attempt to use
|
||||
// it is made after it has already been closed. This cannot happen here,
|
||||
// so we only add a listener for the `'data'` event.
|
||||
//
|
||||
this._deflate.on('data', deflateOnData);
|
||||
}
|
||||
|
||||
this._deflate[kWriteInProgress] = true;
|
||||
|
||||
this._deflate.write(data);
|
||||
this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
|
||||
var data = bufferUtil.concat(
|
||||
this._deflate[kBuffers],
|
||||
this._deflate[kTotalLength]
|
||||
);
|
||||
|
||||
if (fin) data = data.slice(0, data.length - 4);
|
||||
|
||||
if (
|
||||
(fin && this.params[`${endpoint}_no_context_takeover`]) ||
|
||||
this._deflate[kPendingClose]
|
||||
) {
|
||||
this._deflate.close();
|
||||
this._deflate = null;
|
||||
} else {
|
||||
this._deflate[kWriteInProgress] = false;
|
||||
this._deflate[kTotalLength] = 0;
|
||||
this._deflate[kBuffers] = [];
|
||||
}
|
||||
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PerMessageDeflate;
|
||||
|
||||
/**
|
||||
* The listener of the `zlib.DeflateRaw` stream `'data'` event.
|
||||
*
|
||||
* @param {Buffer} chunk A chunk of data
|
||||
* @private
|
||||
*/
|
||||
function deflateOnData (chunk) {
|
||||
this[kBuffers].push(chunk);
|
||||
this[kTotalLength] += chunk.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `zlib.InflateRaw` stream `'data'` event.
|
||||
*
|
||||
* @param {Buffer} chunk A chunk of data
|
||||
* @private
|
||||
*/
|
||||
function inflateOnData (chunk) {
|
||||
this[kTotalLength] += chunk.length;
|
||||
|
||||
if (
|
||||
this[kOwner]._maxPayload < 1 ||
|
||||
this[kTotalLength] <= this[kOwner]._maxPayload
|
||||
) {
|
||||
this[kBuffers].push(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
this[kError] = new Error('max payload size exceeded');
|
||||
this[kError].closeCode = 1009;
|
||||
this.removeListener('data', inflateOnData);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `zlib.InflateRaw` stream `'error'` event.
|
||||
*
|
||||
* @param {Error} err The emitted error
|
||||
* @private
|
||||
*/
|
||||
function inflateOnError (err) {
|
||||
//
|
||||
// There is no need to call `Zlib#close()` as the handle is automatically
|
||||
// closed when an error is emitted.
|
||||
//
|
||||
this[kOwner]._inflate = null;
|
||||
this[kCallback](err);
|
||||
}
|
553
node_modules/ws/lib/Receiver.js
generated
vendored
Normal file
553
node_modules/ws/lib/Receiver.js
generated
vendored
Normal file
@ -0,0 +1,553 @@
|
||||
/*!
|
||||
* 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 perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||
|
||||
perMessageDeflate.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);
|
||||
} else {
|
||||
const buf = toBuffer(fragments, messageLength);
|
||||
|
||||
if (!isValidUTF8(buf)) {
|
||||
this.error(new Error('invalid utf8 sequence'), 1007);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onmessage(buf.toString());
|
||||
}
|
||||
}
|
||||
|
||||
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, '');
|
||||
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());
|
||||
this._loop = false;
|
||||
this.cleanup(this._cleanupCallback);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._opcode === 0x09) this.onping(data);
|
||||
else this.onpong(data);
|
||||
|
||||
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);
|
||||
}
|
412
node_modules/ws/lib/Sender.js
generated
vendored
Normal file
412
node_modules/ws/lib/Sender.js
generated
vendored
Normal file
@ -0,0 +1,412 @@
|
||||
/*!
|
||||
* 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 constants = require('./Constants');
|
||||
|
||||
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._extensions = extensions || {};
|
||||
this._socket = socket;
|
||||
|
||||
this._firstFragment = true;
|
||||
this._compress = false;
|
||||
|
||||
this._bufferedBytes = 0;
|
||||
this._deflating = false;
|
||||
this._queue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Frames a piece of data according to the HyBi WebSocket protocol.
|
||||
*
|
||||
* @param {Buffer} data The data to frame
|
||||
* @param {Object} options Options object
|
||||
* @param {Number} options.opcode The opcode
|
||||
* @param {Boolean} options.readOnly 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) {
|
||||
var buf;
|
||||
|
||||
if (code === undefined) {
|
||||
code = 1000;
|
||||
} else if (typeof code !== 'number' || !ErrorCodes.isValidErrorCode(code)) {
|
||||
throw new Error('first argument must be a valid error code number');
|
||||
}
|
||||
|
||||
if (data === undefined || data === '') {
|
||||
if (code === 1000) {
|
||||
buf = constants.EMPTY_BUFFER;
|
||||
} else {
|
||||
buf = Buffer.allocUnsafe(2);
|
||||
buf.writeUInt16BE(code, 0, true);
|
||||
}
|
||||
} else {
|
||||
buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data));
|
||||
buf.writeUInt16BE(code, 0, true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||
|
||||
if (this._firstFragment) {
|
||||
this._firstFragment = false;
|
||||
if (rsv1 && perMessageDeflate) {
|
||||
rsv1 = data.length >= perMessageDeflate._threshold;
|
||||
}
|
||||
this._compress = rsv1;
|
||||
} else {
|
||||
rsv1 = false;
|
||||
opcode = 0;
|
||||
}
|
||||
|
||||
if (options.fin) this._firstFragment = true;
|
||||
|
||||
if (perMessageDeflate) {
|
||||
const opts = {
|
||||
fin: options.fin,
|
||||
rsv1,
|
||||
opcode,
|
||||
mask: options.mask,
|
||||
readOnly
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||
|
||||
this._deflating = true;
|
||||
perMessageDeflate.compress(data, options.fin, (_, buf) => {
|
||||
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;
|
||||
}
|
17
node_modules/ws/lib/Validation.js
generated
vendored
Normal file
17
node_modules/ws/lib/Validation.js
generated
vendored
Normal file
@ -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;
|
||||
}
|
717
node_modules/ws/lib/WebSocket.js
generated
vendored
Normal file
717
node_modules/ws/lib/WebSocket.js
generated
vendored
Normal file
@ -0,0 +1,717 @@
|
||||
/*!
|
||||
* 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._closeFrameReceived = false;
|
||||
this._closeFrameSent = false;
|
||||
this._closeMessage = '';
|
||||
this._closeTimer = null;
|
||||
this._finalized = false;
|
||||
this._closeCode = 1006;
|
||||
this._receiver = null;
|
||||
this._sender = null;
|
||||
this._socket = null;
|
||||
this._ultron = null;
|
||||
|
||||
if (Array.isArray(address)) {
|
||||
initAsServerClient.call(this, address[0], address[1], 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;
|
||||
|
||||
this._ultron.on('close', this._finalize);
|
||||
this._ultron.on('error', this._finalize);
|
||||
this._ultron.on('end', this._finalize);
|
||||
|
||||
if (head.length > 0) socket.unshift(head);
|
||||
|
||||
this._ultron.on('data', (data) => {
|
||||
this.bytesReceived += data.length;
|
||||
this._receiver.add(data);
|
||||
});
|
||||
|
||||
this._receiver.onmessage = (data) => this.emit('message', data);
|
||||
this._receiver.onping = (data) => {
|
||||
this.pong(data, !this._isServer, true);
|
||||
this.emit('ping', data);
|
||||
};
|
||||
this._receiver.onpong = (data) => this.emit('pong', data);
|
||||
this._receiver.onclose = (code, reason) => {
|
||||
this._closeFrameReceived = true;
|
||||
this._closeMessage = reason;
|
||||
this._closeCode = code;
|
||||
if (!this._finalized) this.close(code, reason);
|
||||
};
|
||||
this._receiver.onerror = (error, code) => {
|
||||
this._closeMessage = '';
|
||||
this._closeCode = code;
|
||||
|
||||
//
|
||||
// Ensure that the error is emitted even if `WebSocket#finalize()` has
|
||||
// already been called.
|
||||
//
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
this.emit('error', error);
|
||||
this.finalize(true);
|
||||
};
|
||||
|
||||
this.readyState = WebSocket.OPEN;
|
||||
this.emit('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up and release internal resources.
|
||||
*
|
||||
* @param {(Boolean|Error)} error Indicates whether or not an error occurred
|
||||
* @private
|
||||
*/
|
||||
finalize (error) {
|
||||
if (this._finalized) return;
|
||||
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
this._finalized = true;
|
||||
|
||||
if (typeof error === 'object') this.emit('error', error);
|
||||
if (!this._socket) return this.emitClose();
|
||||
|
||||
clearTimeout(this._closeTimer);
|
||||
this._closeTimer = null;
|
||||
|
||||
this._ultron.destroy();
|
||||
this._ultron = null;
|
||||
|
||||
this._socket.on('error', constants.NOOP);
|
||||
|
||||
if (!error) this._socket.end();
|
||||
else this._socket.destroy();
|
||||
|
||||
this._socket = null;
|
||||
this._sender = null;
|
||||
|
||||
this._receiver.cleanup(() => this.emitClose());
|
||||
this._receiver = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the `close` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
emitClose () {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
|
||||
this.emit('close', this._closeCode, this._closeMessage);
|
||||
|
||||
if (this.extensions[PerMessageDeflate.extensionName]) {
|
||||
this.extensions[PerMessageDeflate.extensionName].cleanup();
|
||||
}
|
||||
|
||||
this.extensions = null;
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* +----------+ +-----------+ +----------+
|
||||
* + - - -|ws.close()|---->|close frame|-->|ws.close()|- - - -
|
||||
* +----------+ +-----------+ +----------+ |
|
||||
* | +----------+ +-----------+ |
|
||||
* |ws.close()|<----|close frame|<--------+ |
|
||||
* +----------+ +-----------+ |
|
||||
* CLOSING | +---+ | CLOSING
|
||||
* | +---|fin|<------------+
|
||||
* | | | +---+ |
|
||||
* | | +---+ +-------------+
|
||||
* | +----------+-->|fin|----->|ws.finalize()| - - +
|
||||
* | +---+ +-------------+
|
||||
* | +-------------+ |
|
||||
* - - -|ws.finalize()|<--+
|
||||
* +-------------+
|
||||
*
|
||||
* @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) {
|
||||
this._req.abort();
|
||||
this.finalize(new Error('closed before the connection is established'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readyState === WebSocket.CLOSING) {
|
||||
if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
this._sender.close(code, data, !this._isServer, (err) => {
|
||||
//
|
||||
// This error is handled by the `'error'` listener on the socket. We only
|
||||
// want to know if the close frame has been sent here.
|
||||
//
|
||||
if (err) return;
|
||||
|
||||
this._closeFrameSent = true;
|
||||
|
||||
if (!this._finalized) {
|
||||
if (this._closeFrameReceived) this._socket.end();
|
||||
|
||||
//
|
||||
// Ensure that the connection is cleaned up even when the closing
|
||||
// handshake fails.
|
||||
//
|
||||
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) {
|
||||
this._req.abort();
|
||||
this.finalize(new Error('closed before the connection is established'));
|
||||
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 (socket, head, options) {
|
||||
this.protocolVersion = options.protocolVersion;
|
||||
this._maxPayload = options.maxPayload;
|
||||
this.extensions = options.extensions;
|
||||
this.protocol = options.protocol;
|
||||
|
||||
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 {Number} options.handshakeTimeout Timeout in milliseconds for the handshake request
|
||||
* @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} options.ecdhCurve The curves for ECDH key agreement 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,
|
||||
handshakeTimeout: null,
|
||||
localAddress: null,
|
||||
headers: null,
|
||||
family: null,
|
||||
origin: null,
|
||||
agent: null,
|
||||
host: null,
|
||||
|
||||
//
|
||||
// SSL options.
|
||||
//
|
||||
checkServerIdentity: null,
|
||||
rejectUnauthorized: null,
|
||||
passphrase: null,
|
||||
ciphers: null,
|
||||
ecdhCurve: 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;
|
||||
var perMessageDeflate;
|
||||
|
||||
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 (options.perMessageDeflate) {
|
||||
perMessageDeflate = new PerMessageDeflate(
|
||||
options.perMessageDeflate !== true ? options.perMessageDeflate : {},
|
||||
false
|
||||
);
|
||||
requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format({
|
||||
[PerMessageDeflate.extensionName]: perMessageDeflate.offer()
|
||||
});
|
||||
}
|
||||
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.ecdhCurve ||
|
||||
options.cert ||
|
||||
options.key ||
|
||||
options.pfx ||
|
||||
options.ca
|
||||
) {
|
||||
if (options.passphrase) requestOptions.passphrase = options.passphrase;
|
||||
if (options.ciphers) requestOptions.ciphers = options.ciphers;
|
||||
if (options.ecdhCurve) requestOptions.ecdhCurve = options.ecdhCurve;
|
||||
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);
|
||||
|
||||
if (options.handshakeTimeout) {
|
||||
this._req.setTimeout(options.handshakeTimeout, () => {
|
||||
this._req.abort();
|
||||
this.finalize(new Error('opening handshake has timed out'));
|
||||
});
|
||||
}
|
||||
|
||||
this._req.on('error', (error) => {
|
||||
if (this._req.aborted) return;
|
||||
|
||||
this._req = null;
|
||||
this.finalize(error);
|
||||
});
|
||||
|
||||
this._req.on('response', (res) => {
|
||||
if (!this.emit('unexpected-response', this._req, res)) {
|
||||
this._req.abort();
|
||||
this.finalize(new Error(`unexpected server response (${res.statusCode})`));
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
return this.finalize(new Error('invalid server key'));
|
||||
}
|
||||
|
||||
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();
|
||||
return this.finalize(new Error(protError));
|
||||
}
|
||||
|
||||
if (serverProt) this.protocol = serverProt;
|
||||
|
||||
if (perMessageDeflate) {
|
||||
try {
|
||||
const serverExtensions = Extensions.parse(
|
||||
res.headers['sec-websocket-extensions']
|
||||
);
|
||||
|
||||
if (serverExtensions[PerMessageDeflate.extensionName]) {
|
||||
perMessageDeflate.accept(
|
||||
serverExtensions[PerMessageDeflate.extensionName]
|
||||
);
|
||||
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||
}
|
||||
} catch (err) {
|
||||
socket.destroy();
|
||||
this.finalize(new Error('invalid Sec-WebSocket-Extensions header'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setSocket(socket, head);
|
||||
});
|
||||
}
|
326
node_modules/ws/lib/WebSocketServer.js
generated
vendored
Normal file
326
node_modules/ws/lib/WebSocketServer.js
generated
vendored
Normal file
@ -0,0 +1,326 @@
|
||||
/*!
|
||||
* 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: false,
|
||||
handleProtocols: null,
|
||||
clientTracking: true,
|
||||
verifyClient: null,
|
||||
noServer: false,
|
||||
backlog: null, // use default (511 as implemented in net.js)
|
||||
server: null,
|
||||
host: null,
|
||||
path: null,
|
||||
port: null
|
||||
}, options);
|
||||
|
||||
if (options.port == null && !options.server && !options.noServer) {
|
||||
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.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', client, req);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
|
||||
if (options.clientTracking) this.clients = new Set();
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
const extensions = {};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.options.perMessageDeflate) {
|
||||
const perMessageDeflate = new PerMessageDeflate(
|
||||
this.options.perMessageDeflate,
|
||||
true,
|
||||
this.options.maxPayload
|
||||
);
|
||||
|
||||
try {
|
||||
const offers = Extensions.parse(
|
||||
req.headers['sec-websocket-extensions']
|
||||
);
|
||||
|
||||
if (offers[PerMessageDeflate.extensionName]) {
|
||||
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
|
||||
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||
}
|
||||
} catch (err) {
|
||||
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,
|
||||
extensions,
|
||||
version,
|
||||
req,
|
||||
socket,
|
||||
head,
|
||||
cb
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.options.verifyClient(info)) return abortConnection(socket, 401);
|
||||
}
|
||||
|
||||
this.completeUpgrade(protocol, extensions, version, req, socket, head, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the connection to WebSocket.
|
||||
*
|
||||
* @param {String} protocol The chosen subprotocol
|
||||
* @param {Object} extensions The accepted extensions
|
||||
* @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, extensions, 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}`);
|
||||
if (extensions[PerMessageDeflate.extensionName]) {
|
||||
const params = extensions[PerMessageDeflate.extensionName].params;
|
||||
const value = Extensions.format({
|
||||
[PerMessageDeflate.extensionName]: [params]
|
||||
});
|
||||
headers.push(`Sec-WebSocket-Extensions: ${value}`);
|
||||
}
|
||||
|
||||
//
|
||||
// Allow external modification/inspection of handshake headers.
|
||||
//
|
||||
this.emit('headers', headers, req);
|
||||
|
||||
socket.write(headers.concat('\r\n').join('\r\n'));
|
||||
|
||||
const client = new WebSocket([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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
Reference in New Issue
Block a user