Comparing version 4.1.0 to 5.0.0
'use strict'; | ||
const safeBuffer = require('safe-buffer'); | ||
const Buffer = safeBuffer.Buffer; | ||
/** | ||
@@ -8,0 +4,0 @@ * Merges an array of buffers into a new buffer. |
'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 = () => {}; | ||
module.exports = { | ||
BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], | ||
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', | ||
kStatusCode: Symbol('status-code'), | ||
kWebSocket: Symbol('websocket'), | ||
EMPTY_BUFFER: Buffer.alloc(0), | ||
NOOP: () => {} | ||
}; |
'use strict'; | ||
const safeBuffer = require('safe-buffer'); | ||
const Limiter = require('async-limiter'); | ||
@@ -8,8 +7,8 @@ const zlib = require('zlib'); | ||
const bufferUtil = require('./buffer-util'); | ||
const constants = require('./constants'); | ||
const Buffer = safeBuffer.Buffer; | ||
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); | ||
const EMPTY_BLOCK = Buffer.from([0x00]); | ||
const kPerMessageDeflate = Symbol('permessage-deflate'); | ||
const kWriteInProgress = Symbol('write-in-progress'); | ||
@@ -21,3 +20,2 @@ const kPendingClose = Symbol('pending-close'); | ||
const kError = Symbol('error'); | ||
const kOwner = Symbol('owner'); | ||
@@ -351,11 +349,7 @@ // | ||
this._inflate = zlib.createInflateRaw( | ||
Object.assign( | ||
{}, | ||
this._options.zlibInflateOptions, | ||
{ windowBits } | ||
) | ||
Object.assign({}, this._options.zlibInflateOptions, { windowBits }) | ||
); | ||
this._inflate[kPerMessageDeflate] = this; | ||
this._inflate[kTotalLength] = 0; | ||
this._inflate[kBuffers] = []; | ||
this._inflate[kOwner] = this; | ||
this._inflate.on('error', inflateOnError); | ||
@@ -498,4 +492,4 @@ this._inflate.on('data', inflateOnData); | ||
if ( | ||
this[kOwner]._maxPayload < 1 || | ||
this[kTotalLength] <= this[kOwner]._maxPayload | ||
this[kPerMessageDeflate]._maxPayload < 1 || | ||
this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload | ||
) { | ||
@@ -507,3 +501,3 @@ this[kBuffers].push(chunk); | ||
this[kError] = new RangeError('Max payload size exceeded'); | ||
this[kError].closeCode = 1009; | ||
this[kError][constants.kStatusCode] = 1009; | ||
this.removeListener('data', inflateOnData); | ||
@@ -524,4 +518,5 @@ this.reset(); | ||
// | ||
this[kOwner]._inflate = null; | ||
this[kPerMessageDeflate]._inflate = null; | ||
err[constants.kStatusCode] = 1007; | ||
this[kCallback](err); | ||
} |
'use strict'; | ||
const safeBuffer = require('safe-buffer'); | ||
const stream = require('stream'); | ||
@@ -10,4 +10,2 @@ const PerMessageDeflate = require('./permessage-deflate'); | ||
const Buffer = safeBuffer.Buffer; | ||
const GET_INFO = 0; | ||
@@ -22,13 +20,18 @@ const GET_PAYLOAD_LENGTH_16 = 1; | ||
* HyBi Receiver implementation. | ||
* | ||
* @extends stream.Writable | ||
*/ | ||
class Receiver { | ||
class Receiver extends stream.Writable { | ||
/** | ||
* Creates a Receiver instance. | ||
* | ||
* @param {String} binaryType The type for binary data | ||
* @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) { | ||
constructor (binaryType, extensions, maxPayload) { | ||
super(); | ||
this._binaryType = binaryType || constants.BINARY_TYPES[0]; | ||
this[constants.kWebSocket] = undefined; | ||
this._extensions = extensions || {}; | ||
@@ -42,6 +45,6 @@ this._maxPayload = maxPayload | 0; | ||
this._payloadLength = 0; | ||
this._mask = undefined; | ||
this._fragmented = 0; | ||
this._masked = false; | ||
this._fin = false; | ||
this._mask = null; | ||
this._opcode = 0; | ||
@@ -53,32 +56,29 @@ | ||
this._cleanupCallback = null; | ||
this._isCleaningUp = false; | ||
this._hadError = false; | ||
this._state = GET_INFO; | ||
this._loop = false; | ||
} | ||
this.add = this.add.bind(this); | ||
this.onmessage = null; | ||
this.onclose = null; | ||
this.onerror = null; | ||
this.onping = null; | ||
this.onpong = null; | ||
/** | ||
* Implements `Writable.prototype._write()`. | ||
* | ||
* @param {Buffer} chunk The chunk of data to write | ||
* @param {String} encoding The character encoding of `chunk` | ||
* @param {Function} cb Callback | ||
*/ | ||
_write (chunk, encoding, cb) { | ||
if (this._opcode === 0x08) return cb(); | ||
this._state = GET_INFO; | ||
this._bufferedBytes += chunk.length; | ||
this._buffers.push(chunk); | ||
this.startLoop(cb); | ||
} | ||
/** | ||
* Consumes `n` bytes from the buffered data, calls `cleanup` if necessary. | ||
* Consumes `n` bytes from the buffered data. | ||
* | ||
* @param {Number} n The number of bytes to consume | ||
* @return {(Buffer|null)} The consumed bytes or `null` if `n` bytes are not | ||
* available | ||
* @return {Buffer} The consumed bytes | ||
* @private | ||
*/ | ||
consume (n) { | ||
if (this._bufferedBytes < n) { | ||
this._loop = false; | ||
if (this._isCleaningUp) this.cleanup(this._cleanupCallback); | ||
return null; | ||
} | ||
this._bufferedBytes -= n; | ||
@@ -113,19 +113,9 @@ | ||
/** | ||
* Adds new data to the parser. | ||
* | ||
* @param {Buffer} chunk A chunk of data | ||
* @public | ||
*/ | ||
add (chunk) { | ||
this._bufferedBytes += chunk.length; | ||
this._buffers.push(chunk); | ||
this.startLoop(); | ||
} | ||
/** | ||
* Starts the parsing loop. | ||
* | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
startLoop () { | ||
startLoop (cb) { | ||
var err; | ||
this._loop = true; | ||
@@ -136,9 +126,9 @@ | ||
case GET_INFO: | ||
this.getInfo(); | ||
err = this.getInfo(); | ||
break; | ||
case GET_PAYLOAD_LENGTH_16: | ||
this.getPayloadLength16(); | ||
err = this.getPayloadLength16(); | ||
break; | ||
case GET_PAYLOAD_LENGTH_64: | ||
this.getPayloadLength64(); | ||
err = this.getPayloadLength64(); | ||
break; | ||
@@ -149,8 +139,11 @@ case GET_MASK: | ||
case GET_DATA: | ||
this.getData(); | ||
err = this.getData(cb); | ||
break; | ||
default: // `INFLATING` | ||
this._loop = false; | ||
return; | ||
} | ||
} while (this._loop); | ||
cb(err); | ||
} | ||
@@ -161,14 +154,16 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @private | ||
*/ | ||
getInfo () { | ||
if (this._bufferedBytes < 2) { | ||
this._loop = false; | ||
return; | ||
} | ||
const buf = this.consume(2); | ||
if (buf === null) return; | ||
if ((buf[0] & 0x30) !== 0x00) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: RSV2 and RSV3 must be clear'), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); | ||
} | ||
@@ -179,7 +174,4 @@ | ||
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: RSV1 must be clear'), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, 'RSV1 must be clear', true, 1002); | ||
} | ||
@@ -193,27 +185,16 @@ | ||
if (compressed) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: RSV1 must be clear'), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, 'RSV1 must be clear', true, 1002); | ||
} | ||
if (!this._fragmented) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: invalid opcode 0'), | ||
1002 | ||
); | ||
return; | ||
} else { | ||
this._opcode = this._fragmented; | ||
this._loop = false; | ||
return error(RangeError, 'invalid opcode 0', true, 1002); | ||
} | ||
this._opcode = this._fragmented; | ||
} else if (this._opcode === 0x01 || this._opcode === 0x02) { | ||
if (this._fragmented) { | ||
this.error( | ||
new RangeError( | ||
`Invalid WebSocket frame: invalid opcode ${this._opcode}` | ||
), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); | ||
} | ||
@@ -224,39 +205,26 @@ | ||
if (!this._fin) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: FIN must be set'), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, 'FIN must be set', true, 1002); | ||
} | ||
if (compressed) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: RSV1 must be clear'), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, 'RSV1 must be clear', true, 1002); | ||
} | ||
if (this._payloadLength > 0x7d) { | ||
this.error( | ||
new RangeError( | ||
`Invalid WebSocket frame: invalid payload length ` + | ||
`${this._payloadLength}` | ||
), | ||
this._loop = false; | ||
return error( | ||
RangeError, | ||
`invalid payload length ${this._payloadLength}`, | ||
true, | ||
1002 | ||
); | ||
return; | ||
} | ||
} else { | ||
this.error( | ||
new RangeError( | ||
`Invalid WebSocket frame: invalid opcode ${this._opcode}` | ||
), | ||
1002 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); | ||
} | ||
if (!this._fin && !this._fragmented) this._fragmented = this._opcode; | ||
this._masked = (buf[1] & 0x80) === 0x80; | ||
@@ -266,3 +234,3 @@ | ||
else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; | ||
else this.haveLength(); | ||
else return this.haveLength(); | ||
} | ||
@@ -273,10 +241,13 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @private | ||
*/ | ||
getPayloadLength16 () { | ||
const buf = this.consume(2); | ||
if (buf === null) return; | ||
if (this._bufferedBytes < 2) { | ||
this._loop = false; | ||
return; | ||
} | ||
this._payloadLength = buf.readUInt16BE(0, true); | ||
this.haveLength(); | ||
this._payloadLength = this.consume(2).readUInt16BE(0, true); | ||
return this.haveLength(); | ||
} | ||
@@ -287,8 +258,12 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @private | ||
*/ | ||
getPayloadLength64 () { | ||
if (this._bufferedBytes < 8) { | ||
this._loop = false; | ||
return; | ||
} | ||
const buf = this.consume(8); | ||
if (buf === null) return; | ||
const num = buf.readUInt32BE(0, true); | ||
@@ -301,13 +276,13 @@ | ||
if (num > Math.pow(2, 53 - 32) - 1) { | ||
this.error( | ||
new RangeError( | ||
'Unsupported WebSocket frame: payload length > 2^53 - 1' | ||
), | ||
this._loop = false; | ||
return error( | ||
RangeError, | ||
'Unsupported WebSocket frame: payload length > 2^53 - 1', | ||
false, | ||
1009 | ||
); | ||
return; | ||
} | ||
this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4, true); | ||
this.haveLength(); | ||
return this.haveLength(); | ||
} | ||
@@ -318,7 +293,12 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @private | ||
*/ | ||
haveLength () { | ||
if (this._opcode < 0x08 && this.maxPayloadExceeded(this._payloadLength)) { | ||
return; | ||
if (this._payloadLength && this._opcode < 0x08) { | ||
this._totalPayloadLength += this._payloadLength; | ||
if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { | ||
this._loop = false; | ||
return error(RangeError, 'Max payload size exceeded', false, 1009); | ||
} | ||
} | ||
@@ -336,5 +316,8 @@ | ||
getMask () { | ||
if (this._bufferedBytes < 4) { | ||
this._loop = false; | ||
return; | ||
} | ||
this._mask = this.consume(4); | ||
if (this._mask === null) return; | ||
this._state = GET_DATA; | ||
@@ -346,22 +329,37 @@ } | ||
* | ||
* @param {Function} cb Callback | ||
* @return {(Error|RangeError|undefined)} A possible error | ||
* @private | ||
*/ | ||
getData () { | ||
getData (cb) { | ||
var data = constants.EMPTY_BUFFER; | ||
if (this._payloadLength) { | ||
if (this._bufferedBytes < this._payloadLength) { | ||
this._loop = false; | ||
return; | ||
} | ||
data = this.consume(this._payloadLength); | ||
if (data === null) return; | ||
if (this._masked) bufferUtil.unmask(data, this._mask); | ||
} | ||
if (this._opcode > 0x07) { | ||
this.controlMessage(data); | ||
} else if (this._compressed) { | ||
if (this._opcode > 0x07) return this.controlMessage(data); | ||
if (this._compressed) { | ||
this._state = INFLATING; | ||
this.decompress(data); | ||
} else if (this.pushFragment(data)) { | ||
this.dataMessage(); | ||
this.decompress(data, cb); | ||
return; | ||
} | ||
if (data.length) { | ||
// | ||
// This message is not compressed so its lenght is the sum of the payload | ||
// length of all fragments. | ||
// | ||
this._messageLength = this._totalPayloadLength; | ||
this._fragments.push(data); | ||
} | ||
return this.dataMessage(); | ||
} | ||
@@ -373,15 +371,24 @@ | ||
* @param {Buffer} data Compressed data | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
decompress (data) { | ||
decompress (data, cb) { | ||
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 (err) return cb(err); | ||
if (buf.length) { | ||
this._messageLength += buf.length; | ||
if (this._messageLength > this._maxPayload && this._maxPayload > 0) { | ||
return cb(error(RangeError, 'Max payload size exceeded', false, 1009)); | ||
} | ||
this._fragments.push(buf); | ||
} | ||
if (this.pushFragment(buf)) this.dataMessage(); | ||
this.startLoop(); | ||
const er = this.dataMessage(); | ||
if (er) return cb(er); | ||
this.startLoop(cb); | ||
}); | ||
@@ -393,2 +400,3 @@ } | ||
* | ||
* @return {(Error|undefined)} A possible error | ||
* @private | ||
@@ -417,3 +425,3 @@ */ | ||
this.onmessage(data); | ||
this.emit('message', data); | ||
} else { | ||
@@ -423,10 +431,7 @@ const buf = toBuffer(fragments, messageLength); | ||
if (!validation.isValidUTF8(buf)) { | ||
this.error( | ||
new Error('Invalid WebSocket frame: invalid UTF-8 sequence'), | ||
1007 | ||
); | ||
return; | ||
this._loop = false; | ||
return error(Error, 'invalid UTF-8 sequence', true, 1007); | ||
} | ||
this.onmessage(buf.toString()); | ||
this.emit('message', buf.toString()); | ||
} | ||
@@ -442,2 +447,3 @@ } | ||
* @param {Buffer} data Data to handle | ||
* @return {(Error|RangeError|undefined)} A possible error | ||
* @private | ||
@@ -447,11 +453,9 @@ */ | ||
if (this._opcode === 0x08) { | ||
this._loop = false; | ||
if (data.length === 0) { | ||
this._loop = false; | ||
this.onclose(1005, ''); | ||
this.cleanup(this._cleanupCallback); | ||
this.emit('conclude', 1005, ''); | ||
this.end(); | ||
} else if (data.length === 1) { | ||
this.error( | ||
new RangeError('Invalid WebSocket frame: invalid payload length 1'), | ||
1002 | ||
); | ||
return error(RangeError, 'invalid payload length 1', true, 1002); | ||
} else { | ||
@@ -461,9 +465,3 @@ const code = data.readUInt16BE(0, true); | ||
if (!validation.isValidStatusCode(code)) { | ||
this.error( | ||
new RangeError( | ||
`Invalid WebSocket frame: invalid status code ${code}` | ||
), | ||
1002 | ||
); | ||
return; | ||
return error(RangeError, `invalid status code ${code}`, true, 1002); | ||
} | ||
@@ -474,12 +472,7 @@ | ||
if (!validation.isValidUTF8(buf)) { | ||
this.error( | ||
new Error('Invalid WebSocket frame: invalid UTF-8 sequence'), | ||
1007 | ||
); | ||
return; | ||
return error(Error, 'invalid UTF-8 sequence', true, 1007); | ||
} | ||
this._loop = false; | ||
this.onclose(code, buf.toString()); | ||
this.cleanup(this._cleanupCallback); | ||
this.emit('conclude', code, buf.toString()); | ||
this.end(); | ||
} | ||
@@ -490,101 +483,32 @@ | ||
if (this._opcode === 0x09) this.onping(data); | ||
else this.onpong(data); | ||
if (this._opcode === 0x09) this.emit('ping', data); | ||
else this.emit('pong', data); | ||
this._state = GET_INFO; | ||
} | ||
} | ||
/** | ||
* Handles an error. | ||
* | ||
* @param {Error} err The error | ||
* @param {Number} code Close code | ||
* @private | ||
*/ | ||
error (err, code) { | ||
this._hadError = true; | ||
this._loop = false; | ||
this.onerror(err, code); | ||
this.cleanup(this._cleanupCallback); | ||
} | ||
module.exports = Receiver; | ||
/** | ||
* 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; | ||
/** | ||
* Builds an error object. | ||
* | ||
* @param {(Error|RangeError)} ErrorCtor The error constructor | ||
* @param {String} message The error message | ||
* @param {Boolean} prefix Specifies whether or not to add a default prefix to | ||
* `message` | ||
* @param {Number} statusCode The status code | ||
* @return {(Error|RangeError)} The error | ||
* @private | ||
*/ | ||
function error (ErrorCtor, message, prefix, statusCode) { | ||
const err = new ErrorCtor( | ||
prefix ? `Invalid WebSocket frame: ${message}` : message | ||
); | ||
const fullLength = this._totalPayloadLength + length; | ||
if (fullLength <= this._maxPayload) { | ||
this._totalPayloadLength = fullLength; | ||
return false; | ||
} | ||
this.error(new RangeError('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 RangeError('Max payload size exceeded'), 1009); | ||
return false; | ||
} | ||
/** | ||
* Releases resources used by the receiver. | ||
* | ||
* @param {Function} cb Callback | ||
* @public | ||
*/ | ||
cleanup (cb) { | ||
if (this._extensions === null) { | ||
if (cb) cb(); | ||
return; | ||
} | ||
if (!this._hadError && (this._loop || this._state === INFLATING)) { | ||
this._cleanupCallback = cb; | ||
this._isCleaningUp = true; | ||
return; | ||
} | ||
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(); | ||
} | ||
Error.captureStackTrace(err, error); | ||
err[constants.kStatusCode] = statusCode; | ||
return err; | ||
} | ||
module.exports = Receiver; | ||
/** | ||
@@ -591,0 +515,0 @@ * Makes a buffer from a list of fragments. |
'use strict'; | ||
const safeBuffer = require('safe-buffer'); | ||
const crypto = require('crypto'); | ||
@@ -11,4 +10,2 @@ | ||
const Buffer = safeBuffer.Buffer; | ||
/** | ||
@@ -15,0 +12,0 @@ * HyBi Sender implementation. |
'use strict'; | ||
const safeBuffer = require('safe-buffer'); | ||
const EventEmitter = require('events'); | ||
@@ -14,4 +13,2 @@ const crypto = require('crypto'); | ||
const Buffer = safeBuffer.Buffer; | ||
/** | ||
@@ -176,3 +173,3 @@ * Class representing a WebSocket server. | ||
) { | ||
return abortConnection(socket, 400); | ||
return abortHandshake(socket, 400); | ||
} | ||
@@ -197,19 +194,7 @@ | ||
} catch (err) { | ||
return abortConnection(socket, 400); | ||
return abortHandshake(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. | ||
@@ -226,5 +211,5 @@ // | ||
this.options.verifyClient(info, (verified, code, message) => { | ||
if (!verified) return abortConnection(socket, code || 401, message); | ||
if (!verified) return abortHandshake(socket, code || 401, message); | ||
this.completeUpgrade(protocol, extensions, req, socket, head, cb); | ||
this.completeUpgrade(extensions, req, socket, head, cb); | ||
}); | ||
@@ -234,6 +219,6 @@ return; | ||
if (!this.options.verifyClient(info)) return abortConnection(socket, 401); | ||
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); | ||
} | ||
this.completeUpgrade(protocol, extensions, req, socket, head, cb); | ||
this.completeUpgrade(extensions, req, socket, head, cb); | ||
} | ||
@@ -244,3 +229,2 @@ | ||
* | ||
* @param {String} protocol The chosen subprotocol | ||
* @param {Object} extensions The accepted extensions | ||
@@ -253,3 +237,3 @@ * @param {http.IncomingMessage} req The request object | ||
*/ | ||
completeUpgrade (protocol, extensions, req, socket, head, cb) { | ||
completeUpgrade (extensions, req, socket, head, cb) { | ||
// | ||
@@ -272,7 +256,22 @@ // Destroy the socket if the client has already sent a FIN packet. | ||
const ws = new WebSocket(null); | ||
var protocol = req.headers['sec-websocket-protocol']; | ||
if (protocol) { | ||
headers.push(`Sec-WebSocket-Protocol: ${protocol}`); | ||
ws.protocol = protocol; | ||
protocol = protocol.trim().split(/ *, */); | ||
// | ||
// Optionally call external protocol selection handler. | ||
// | ||
if (this.options.handleProtocols) { | ||
protocol = this.options.handleProtocols(protocol, req); | ||
} else { | ||
protocol = protocol[0]; | ||
} | ||
if (protocol) { | ||
headers.push(`Sec-WebSocket-Protocol: ${protocol}`); | ||
ws.protocol = protocol; | ||
} | ||
} | ||
if (extensions[PerMessageDeflate.extensionName]) { | ||
@@ -344,3 +343,3 @@ const params = extensions[PerMessageDeflate.extensionName].params; | ||
*/ | ||
function abortConnection (socket, code, message) { | ||
function abortHandshake (socket, code, message) { | ||
if (socket.writable) { | ||
@@ -347,0 +346,0 @@ message = message || http.STATUS_CODES[code]; |
@@ -17,2 +17,3 @@ 'use strict'; | ||
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; | ||
const kWebSocket = constants.kWebSocket; | ||
const protocolVersions = [8, 13]; | ||
@@ -41,3 +42,2 @@ const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly. | ||
this._binaryType = constants.BINARY_TYPES[0]; | ||
this._finalize = this.finalize.bind(this); | ||
this._closeFrameReceived = false; | ||
@@ -47,3 +47,2 @@ this._closeFrameSent = false; | ||
this._closeTimer = null; | ||
this._finalized = false; | ||
this._closeCode = 1006; | ||
@@ -55,3 +54,2 @@ this._extensions = {}; | ||
this._socket = null; | ||
this._error = null; | ||
@@ -126,45 +124,32 @@ if (address !== null) { | ||
setSocket (socket, head, maxPayload) { | ||
socket.setTimeout(0); | ||
socket.setNoDelay(); | ||
const receiver = new Receiver( | ||
this._binaryType, | ||
this._extensions, | ||
maxPayload | ||
); | ||
socket.on('close', this._finalize); | ||
socket.on('error', this._finalize); | ||
socket.on('end', this._finalize); | ||
this._receiver = new Receiver(this._extensions, maxPayload, this.binaryType); | ||
this._sender = new Sender(socket, this._extensions); | ||
this._receiver = receiver; | ||
this._socket = socket; | ||
if (head.length > 0) socket.unshift(head); | ||
receiver[kWebSocket] = this; | ||
socket[kWebSocket] = this; | ||
socket.on('data', this._receiver.add); | ||
receiver.on('conclude', receiverOnConclude); | ||
receiver.on('drain', receiverOnDrain); | ||
receiver.on('error', receiverOnError); | ||
receiver.on('message', receiverOnMessage); | ||
receiver.on('ping', receiverOnPing); | ||
receiver.on('pong', receiverOnPong); | ||
this._receiver.onmessage = (data) => this.emit('message', data); | ||
this._receiver.onping = (data) => { | ||
this.pong(data, !this._isServer, constants.NOOP); | ||
this.emit('ping', data); | ||
}; | ||
this._receiver.onpong = (data) => this.emit('pong', data); | ||
this._receiver.onclose = (code, reason) => { | ||
// | ||
// Discard any additional data that is received on the socket. | ||
// | ||
this._socket.removeListener('data', this._receiver.add); | ||
socket.setTimeout(0); | ||
socket.setNoDelay(); | ||
this._closeFrameReceived = true; | ||
this._closeMessage = reason; | ||
this._closeCode = code; | ||
if (head.length > 0) socket.unshift(head); | ||
if (code === 1005) this.close(); | ||
else this.close(code, reason); | ||
}; | ||
this._receiver.onerror = (error, code) => { | ||
if (this._error) return; | ||
socket.on('close', socketOnClose); | ||
socket.on('data', socketOnData); | ||
socket.on('end', socketOnEnd); | ||
socket.on('error', socketOnError); | ||
this._closeCode = code; | ||
if (!this._finalized) this.finalize(error); | ||
else this.emit('error', error); | ||
}; | ||
this.readyState = WebSocket.OPEN; | ||
@@ -175,19 +160,10 @@ this.emit('open'); | ||
/** | ||
* Clean up internal resources and emit the `'close'` event. | ||
* Emit the `'close'` event. | ||
* | ||
* @param {(Boolean|Error)} error Indicates whether or not an error occurred | ||
* @private | ||
*/ | ||
finalize (error) { | ||
if (this._finalized) return; | ||
emitClose () { | ||
this.readyState = WebSocket.CLOSED; | ||
this.readyState = WebSocket.CLOSING; | ||
this._finalized = true; | ||
if (!this._socket) { | ||
// | ||
// `error` is always an `Error` instance in this case. | ||
// | ||
this.emit('error', error); | ||
this.readyState = WebSocket.CLOSED; | ||
this.emit('close', this._closeCode, this._closeMessage); | ||
@@ -197,33 +173,8 @@ return; | ||
clearTimeout(this._closeTimer); | ||
this._socket.removeListener('data', this._receiver.add); | ||
this._socket.removeListener('close', this._finalize); | ||
this._socket.removeListener('error', this._finalize); | ||
this._socket.removeListener('end', this._finalize); | ||
this._socket.on('error', constants.NOOP); | ||
if (error) { | ||
if (error !== true) this._error = error; | ||
this._socket.destroy(); | ||
} else { | ||
this._socket.end(); | ||
if (this._extensions[PerMessageDeflate.extensionName]) { | ||
this._extensions[PerMessageDeflate.extensionName].cleanup(); | ||
} | ||
this._receiver.cleanup(() => { | ||
const err = this._error; | ||
if (err) { | ||
this._error = null; | ||
this.emit('error', err); | ||
} | ||
this.readyState = WebSocket.CLOSED; | ||
if (this._extensions[PerMessageDeflate.extensionName]) { | ||
this._extensions[PerMessageDeflate.extensionName].cleanup(); | ||
} | ||
this.emit('close', this._closeCode, this._closeMessage); | ||
}); | ||
this._receiver.removeAllListeners(); | ||
this.emit('close', this._closeCode, this._closeMessage); | ||
} | ||
@@ -234,17 +185,13 @@ | ||
* | ||
* +----------+ +-----------+ +----------+ | ||
* + - - -|ws.close()|---->|close frame|-->|ws.close()|- - - - | ||
* +----------+ +-----------+ +----------+ | | ||
* | +----------+ +-----------+ | | ||
* |ws.close()|<----|close frame|<--------+ | | ||
* +----------+ +-----------+ | | ||
* CLOSING | +---+ | CLOSING | ||
* | +---|fin|<------------+ | ||
* | | | +---+ | | ||
* | | +---+ +-------------+ | ||
* | +----------+-->|fin|----->|ws.finalize()| - - + | ||
* | +---+ +-------------+ | ||
* | +-------------+ | | ||
* - - -|ws.finalize()|<--+ | ||
* +-------------+ | ||
* +----------+ +-----------+ +----------+ | ||
* - - -|ws.close()|-->|close frame|-->|ws.close()|- - - | ||
* | +----------+ +-----------+ +----------+ | | ||
* +----------+ +-----------+ | | ||
* CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING | ||
* +----------+ +-----------+ | | ||
* | | | +---+ | | ||
* +------------------------+-->|fin| - - - - | ||
* | +---+ | +---+ | ||
* - - - - -|fin|<---------------------+ | ||
* +---+ | ||
* | ||
@@ -258,7 +205,4 @@ * @param {Number} code Status code explaining why the connection is closing | ||
if (this.readyState === WebSocket.CONNECTING) { | ||
this._req.abort(); | ||
this.finalize( | ||
new Error('WebSocket was closed before the connection was established') | ||
); | ||
return; | ||
const msg = 'WebSocket was closed before the connection was established'; | ||
return abortHandshake(this, this._req, msg); | ||
} | ||
@@ -281,10 +225,13 @@ | ||
if (!this._finalized) { | ||
if (this._socket.writable) { | ||
if (this._closeFrameReceived) this._socket.end(); | ||
// | ||
// Ensure that the connection is cleaned up even when the closing | ||
// handshake fails. | ||
// Ensure that the connection is closed even if the closing handshake | ||
// fails. | ||
// | ||
this._closeTimer = setTimeout(this._finalize, closeTimeout, true); | ||
this._closeTimer = setTimeout( | ||
this._socket.destroy.bind(this._socket), | ||
closeTimeout | ||
); | ||
} | ||
@@ -410,10 +357,10 @@ }); | ||
if (this.readyState === WebSocket.CONNECTING) { | ||
this._req.abort(); | ||
this.finalize( | ||
new Error('WebSocket was closed before the connection was established') | ||
); | ||
return; | ||
const msg = 'WebSocket was closed before the connection was established'; | ||
return abortHandshake(this, this._req, msg); | ||
} | ||
this.finalize(true); | ||
if (this._socket) { | ||
this.readyState = WebSocket.CLOSING; | ||
this._socket.destroy(); | ||
} | ||
} | ||
@@ -633,26 +580,27 @@ } | ||
this._req = httpObj.get(requestOptions); | ||
var req = 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')); | ||
}); | ||
req.setTimeout( | ||
options.handshakeTimeout, | ||
abortHandshake.bind(null, this, req, 'Opening handshake has timed out') | ||
); | ||
} | ||
this._req.on('error', (error) => { | ||
req.on('error', (err) => { | ||
if (this._req.aborted) return; | ||
this._req = null; | ||
this.finalize(error); | ||
req = this._req = null; | ||
this.readyState = WebSocket.CLOSING; | ||
this.emit('error', err); | ||
this.emitClose(); | ||
}); | ||
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}`)); | ||
} | ||
req.on('response', (res) => { | ||
if (this.emit('unexpected-response', req, res)) return; | ||
abortHandshake(this, req, `Unexpected server response: ${res.statusCode}`); | ||
}); | ||
this._req.on('upgrade', (res, socket, head) => { | ||
req.on('upgrade', (res, socket, head) => { | ||
this.emit('upgrade', res); | ||
@@ -666,3 +614,3 @@ | ||
this._req = null; | ||
req = this._req = null; | ||
@@ -674,4 +622,4 @@ const digest = crypto.createHash('sha1') | ||
if (res.headers['sec-websocket-accept'] !== digest) { | ||
socket.destroy(); | ||
return this.finalize(new Error('Invalid Sec-WebSocket-Accept header')); | ||
abortHandshake(this, socket, 'Invalid Sec-WebSocket-Accept header'); | ||
return; | ||
} | ||
@@ -692,4 +640,4 @@ | ||
if (protError) { | ||
socket.destroy(); | ||
return this.finalize(new Error(protError)); | ||
abortHandshake(this, socket, protError); | ||
return; | ||
} | ||
@@ -712,4 +660,3 @@ | ||
} catch (err) { | ||
socket.destroy(); | ||
this.finalize(new Error('Invalid Sec-WebSocket-Extensions header')); | ||
abortHandshake(this, socket, 'Invalid Sec-WebSocket-Extensions header'); | ||
return; | ||
@@ -722,1 +669,193 @@ } | ||
} | ||
/** | ||
* Abort the handshake and emit an error. | ||
* | ||
* @param {WebSocket} websocket The WebSocket instance | ||
* @param {(http.ClientRequest|net.Socket)} stream The request to abort or the | ||
* socket to destroy | ||
* @param {String} message The error message | ||
* @private | ||
*/ | ||
function abortHandshake (websocket, stream, message) { | ||
websocket.readyState = WebSocket.CLOSING; | ||
const err = new Error(message); | ||
Error.captureStackTrace(err, abortHandshake); | ||
if (stream.setHeader) { | ||
stream.abort(); | ||
stream.once('abort', websocket.emitClose.bind(websocket)); | ||
websocket.emit('error', err); | ||
} else { | ||
stream.destroy(err); | ||
stream.once('error', websocket.emit.bind(websocket, 'error')); | ||
stream.once('close', websocket.emitClose.bind(websocket)); | ||
} | ||
} | ||
/** | ||
* The listener of the `Receiver` `'conclude'` event. | ||
* | ||
* @param {Number} code The status code | ||
* @param {String} reason The reason for closing | ||
* @private | ||
*/ | ||
function receiverOnConclude (code, reason) { | ||
const websocket = this[kWebSocket]; | ||
websocket._socket.removeListener('data', socketOnData); | ||
websocket._socket.resume(); | ||
websocket._closeFrameReceived = true; | ||
websocket._closeMessage = reason; | ||
websocket._closeCode = code; | ||
if (code === 1005) websocket.close(); | ||
else websocket.close(code, reason); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'drain'` event. | ||
* | ||
* @private | ||
*/ | ||
function receiverOnDrain () { | ||
this[kWebSocket]._socket.resume(); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'error'` event. | ||
* | ||
* @param {(RangeError|Error)} err The emitted error | ||
* @private | ||
*/ | ||
function receiverOnError (err) { | ||
const websocket = this[kWebSocket]; | ||
websocket.readyState = WebSocket.CLOSING; | ||
websocket._closeCode = err[constants.kStatusCode]; | ||
websocket.emit('error', err); | ||
websocket._socket.destroy(); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'finish'` event. | ||
* | ||
* @private | ||
*/ | ||
function receiverOnFinish () { | ||
this[kWebSocket].emitClose(); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'message'` event. | ||
* | ||
* @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message | ||
* @private | ||
*/ | ||
function receiverOnMessage (data) { | ||
this[kWebSocket].emit('message', data); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'ping'` event. | ||
* | ||
* @param {Buffer} data The data included in the ping frame | ||
* @private | ||
*/ | ||
function receiverOnPing (data) { | ||
const websocket = this[kWebSocket]; | ||
websocket.pong(data, !websocket._isServer, constants.NOOP); | ||
websocket.emit('ping', data); | ||
} | ||
/** | ||
* The listener of the `Receiver` `'pong'` event. | ||
* | ||
* @param {Buffer} data The data included in the pong frame | ||
* @private | ||
*/ | ||
function receiverOnPong (data) { | ||
this[kWebSocket].emit('pong', data); | ||
} | ||
/** | ||
* The listener of the `net.Socket` `'close'` event. | ||
* | ||
* @private | ||
*/ | ||
function socketOnClose () { | ||
const websocket = this[kWebSocket]; | ||
this.removeListener('close', socketOnClose); | ||
this.removeListener('data', socketOnData); | ||
this.removeListener('end', socketOnEnd); | ||
this[kWebSocket] = undefined; | ||
websocket.readyState = WebSocket.CLOSING; | ||
// | ||
// The close frame might not have been received or the `'end'` event emitted, | ||
// for example, if the socket was destroyed due to an error. Ensure that the | ||
// `receiver` stream is closed after writing any remaining buffered data to | ||
// it. | ||
// | ||
websocket._socket.read(); | ||
websocket._receiver.end(); | ||
clearTimeout(websocket._closeTimer); | ||
if ( | ||
websocket._receiver._writableState.finished || | ||
websocket._receiver._writableState.errorEmitted | ||
) { | ||
websocket.emitClose(); | ||
} else { | ||
websocket._receiver.on('error', receiverOnFinish); | ||
websocket._receiver.on('finish', receiverOnFinish); | ||
} | ||
} | ||
/** | ||
* The listener of the `net.Socket` `'data'` event. | ||
* | ||
* @param {Buffer} chunk A chunk of data | ||
* @private | ||
*/ | ||
function socketOnData (chunk) { | ||
if (!this[kWebSocket]._receiver.write(chunk)) { | ||
this.pause(); | ||
} | ||
} | ||
/** | ||
* The listener of the `net.Socket` `'end'` event. | ||
* | ||
* @private | ||
*/ | ||
function socketOnEnd () { | ||
const websocket = this[kWebSocket]; | ||
websocket.readyState = WebSocket.CLOSING; | ||
websocket._receiver.end(); | ||
this.end(); | ||
} | ||
/** | ||
* The listener of the `net.Socket` `'error'` event. | ||
* | ||
* @private | ||
*/ | ||
function socketOnError () { | ||
const websocket = this[kWebSocket]; | ||
this.removeListener('error', socketOnError); | ||
this.on('error', constants.NOOP); | ||
if (websocket) { | ||
websocket.readyState = WebSocket.CLOSING; | ||
this.destroy(); | ||
} | ||
} |
{ | ||
"name": "ws", | ||
"version": "4.1.0", | ||
"version": "5.0.0", | ||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", | ||
@@ -29,4 +29,3 @@ "keywords": [ | ||
"dependencies": { | ||
"async-limiter": "~1.0.0", | ||
"safe-buffer": "~5.1.0" | ||
"async-limiter": "~1.0.0" | ||
}, | ||
@@ -33,0 +32,0 @@ "devDependencies": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
105211
1
2713
- Removedsafe-buffer@~5.1.0
- Removedsafe-buffer@5.1.2(transitive)