Comparing version 8.4.0 to 8.16.0
@@ -5,2 +5,4 @@ 'use strict'; | ||
const FastBuffer = Buffer[Symbol.species]; | ||
/** | ||
@@ -27,3 +29,5 @@ * Merges an array of buffers into a new buffer. | ||
if (offset < totalLength) return target.slice(0, offset); | ||
if (offset < totalLength) { | ||
return new FastBuffer(target.buffer, target.byteOffset, offset); | ||
} | ||
@@ -70,7 +74,7 @@ return target; | ||
function toArrayBuffer(buf) { | ||
if (buf.byteLength === buf.buffer.byteLength) { | ||
if (buf.length === buf.buffer.byteLength) { | ||
return buf.buffer; | ||
} | ||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); | ||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); | ||
} | ||
@@ -94,5 +98,5 @@ | ||
if (data instanceof ArrayBuffer) { | ||
buf = Buffer.from(data); | ||
buf = new FastBuffer(data); | ||
} else if (ArrayBuffer.isView(data)) { | ||
buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); | ||
buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); | ||
} else { | ||
@@ -106,26 +110,27 @@ buf = Buffer.from(data); | ||
try { | ||
const bufferUtil = require('bufferutil'); | ||
module.exports = { | ||
concat, | ||
mask: _mask, | ||
toArrayBuffer, | ||
toBuffer, | ||
unmask: _unmask | ||
}; | ||
module.exports = { | ||
concat, | ||
mask(source, mask, output, offset, length) { | ||
/* istanbul ignore else */ | ||
if (!process.env.WS_NO_BUFFER_UTIL) { | ||
try { | ||
const bufferUtil = require('bufferutil'); | ||
module.exports.mask = function (source, mask, output, offset, length) { | ||
if (length < 48) _mask(source, mask, output, offset, length); | ||
else bufferUtil.mask(source, mask, output, offset, length); | ||
}, | ||
toArrayBuffer, | ||
toBuffer, | ||
unmask(buffer, mask) { | ||
}; | ||
module.exports.unmask = function (buffer, mask) { | ||
if (buffer.length < 32) _unmask(buffer, mask); | ||
else bufferUtil.unmask(buffer, mask); | ||
} | ||
}; | ||
} catch (e) /* istanbul ignore next */ { | ||
module.exports = { | ||
concat, | ||
mask: _mask, | ||
toArrayBuffer, | ||
toBuffer, | ||
unmask: _unmask | ||
}; | ||
}; | ||
} catch (e) { | ||
// Continue regardless of the error. | ||
} | ||
} |
@@ -181,3 +181,3 @@ 'use strict'; | ||
* @param {String} type A string representing the event type to listen for | ||
* @param {Function} listener The listener to add | ||
* @param {(Function|Object)} handler The listener to add | ||
* @param {Object} [options] An options object specifies characteristics about | ||
@@ -190,3 +190,13 @@ * the event listener | ||
*/ | ||
addEventListener(type, listener, options = {}) { | ||
addEventListener(type, handler, options = {}) { | ||
for (const listener of this.listeners(type)) { | ||
if ( | ||
!options[kForOnEventAttribute] && | ||
listener[kListener] === handler && | ||
!listener[kForOnEventAttribute] | ||
) { | ||
return; | ||
} | ||
} | ||
let wrapper; | ||
@@ -201,3 +211,3 @@ | ||
event[kTarget] = this; | ||
listener.call(this, event); | ||
callListener(handler, this, event); | ||
}; | ||
@@ -213,3 +223,3 @@ } else if (type === 'close') { | ||
event[kTarget] = this; | ||
listener.call(this, event); | ||
callListener(handler, this, event); | ||
}; | ||
@@ -224,3 +234,3 @@ } else if (type === 'error') { | ||
event[kTarget] = this; | ||
listener.call(this, event); | ||
callListener(handler, this, event); | ||
}; | ||
@@ -232,3 +242,3 @@ } else if (type === 'open') { | ||
event[kTarget] = this; | ||
listener.call(this, event); | ||
callListener(handler, this, event); | ||
}; | ||
@@ -240,3 +250,3 @@ } else { | ||
wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; | ||
wrapper[kListener] = listener; | ||
wrapper[kListener] = handler; | ||
@@ -254,3 +264,3 @@ if (options.once) { | ||
* @param {String} type A string representing the event type to remove | ||
* @param {Function} handler The listener to remove | ||
* @param {(Function|Object)} handler The listener to remove | ||
* @public | ||
@@ -275,1 +285,17 @@ */ | ||
}; | ||
/** | ||
* Call an event listener | ||
* | ||
* @param {(Function|Object)} listener The listener to call | ||
* @param {*} thisArg The value to use as `this`` when calling the listener | ||
* @param {Event} event The event to pass to the listener | ||
* @private | ||
*/ | ||
function callListener(listener, thisArg, event) { | ||
if (typeof listener === 'object' && listener.handleEvent) { | ||
listener.handleEvent.call(listener, event); | ||
} else { | ||
listener.call(thisArg, event); | ||
} | ||
} |
@@ -9,2 +9,3 @@ 'use strict'; | ||
const FastBuffer = Buffer[Symbol.species]; | ||
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); | ||
@@ -317,3 +318,3 @@ const kPerMessageDeflate = Symbol('permessage-deflate'); | ||
* | ||
* @param {Buffer} data Data to compress | ||
* @param {(Buffer|String)} data Data to compress | ||
* @param {Boolean} fin Specifies whether or not this is the last fragment | ||
@@ -400,3 +401,3 @@ * @param {Function} callback Callback | ||
* | ||
* @param {Buffer} data Data to compress | ||
* @param {(Buffer|String)} data Data to compress | ||
* @param {Boolean} fin Specifies whether or not this is the last fragment | ||
@@ -443,3 +444,5 @@ * @param {Function} callback Callback | ||
if (fin) data = data.slice(0, data.length - 4); | ||
if (fin) { | ||
data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); | ||
} | ||
@@ -446,0 +449,0 @@ // |
@@ -15,2 +15,11 @@ 'use strict'; | ||
const FastBuffer = Buffer[Symbol.species]; | ||
const promise = Promise.resolve(); | ||
// | ||
// `queueMicrotask()` is not available in Node.js < 11. | ||
// | ||
const queueTask = | ||
typeof queueMicrotask === 'function' ? queueMicrotask : queueMicrotaskShim; | ||
const GET_INFO = 0; | ||
@@ -22,2 +31,3 @@ const GET_PAYLOAD_LENGTH_16 = 1; | ||
const INFLATING = 5; | ||
const DEFER_EVENT = 6; | ||
@@ -34,2 +44,5 @@ /** | ||
* @param {Object} [options] Options object | ||
* @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether | ||
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted | ||
* multiple times in the same tick | ||
* @param {String} [options.binaryType=nodebuffer] The type for binary data | ||
@@ -47,2 +60,3 @@ * @param {Object} [options.extensions] An object containing the negotiated | ||
this._allowSynchronousEvents = !!options.allowSynchronousEvents; | ||
this._binaryType = options.binaryType || BINARY_TYPES[0]; | ||
@@ -70,4 +84,5 @@ this._extensions = options.extensions || {}; | ||
this._errored = false; | ||
this._loop = false; | ||
this._state = GET_INFO; | ||
this._loop = false; | ||
} | ||
@@ -105,4 +120,9 @@ | ||
const buf = this._buffers[0]; | ||
this._buffers[0] = buf.slice(n); | ||
return buf.slice(0, n); | ||
this._buffers[0] = new FastBuffer( | ||
buf.buffer, | ||
buf.byteOffset + n, | ||
buf.length - n | ||
); | ||
return new FastBuffer(buf.buffer, buf.byteOffset, n); | ||
} | ||
@@ -120,3 +140,7 @@ | ||
dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); | ||
this._buffers[0] = buf.slice(n); | ||
this._buffers[0] = new FastBuffer( | ||
buf.buffer, | ||
buf.byteOffset + n, | ||
buf.length - n | ||
); | ||
} | ||
@@ -137,3 +161,2 @@ | ||
startLoop(cb) { | ||
let err; | ||
this._loop = true; | ||
@@ -144,9 +167,9 @@ | ||
case GET_INFO: | ||
err = this.getInfo(); | ||
this.getInfo(cb); | ||
break; | ||
case GET_PAYLOAD_LENGTH_16: | ||
err = this.getPayloadLength16(); | ||
this.getPayloadLength16(cb); | ||
break; | ||
case GET_PAYLOAD_LENGTH_64: | ||
err = this.getPayloadLength64(); | ||
this.getPayloadLength64(cb); | ||
break; | ||
@@ -157,6 +180,6 @@ case GET_MASK: | ||
case GET_DATA: | ||
err = this.getData(cb); | ||
this.getData(cb); | ||
break; | ||
default: | ||
// `INFLATING` | ||
case INFLATING: | ||
case DEFER_EVENT: | ||
this._loop = false; | ||
@@ -167,3 +190,3 @@ return; | ||
cb(err); | ||
if (!this._errored) cb(); | ||
} | ||
@@ -174,6 +197,6 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
getInfo() { | ||
getInfo(cb) { | ||
if (this._bufferedBytes < 2) { | ||
@@ -187,4 +210,3 @@ this._loop = false; | ||
if ((buf[0] & 0x30) !== 0x00) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -196,2 +218,5 @@ 'RSV2 and RSV3 must be clear', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -202,4 +227,3 @@ | ||
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -211,2 +235,5 @@ 'RSV1 must be clear', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -220,4 +247,3 @@ | ||
if (compressed) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -229,7 +255,9 @@ 'RSV1 must be clear', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
if (!this._fragmented) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -241,2 +269,5 @@ 'invalid opcode 0', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -247,4 +278,3 @@ | ||
if (this._fragmented) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -256,2 +286,5 @@ `invalid opcode ${this._opcode}`, | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -262,4 +295,3 @@ | ||
if (!this._fin) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -271,7 +303,9 @@ 'FIN must be set', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
if (compressed) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -283,7 +317,12 @@ 'RSV1 must be clear', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
if (this._payloadLength > 0x7d) { | ||
this._loop = false; | ||
return error( | ||
if ( | ||
this._payloadLength > 0x7d || | ||
(this._opcode === 0x08 && this._payloadLength === 1) | ||
) { | ||
const error = this.createError( | ||
RangeError, | ||
@@ -295,6 +334,8 @@ `invalid payload length ${this._payloadLength}`, | ||
); | ||
cb(error); | ||
return; | ||
} | ||
} else { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -306,2 +347,5 @@ `invalid opcode ${this._opcode}`, | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -314,4 +358,3 @@ | ||
if (!this._masked) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -323,6 +366,8 @@ 'MASK must be set', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
} else if (this._masked) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -334,2 +379,5 @@ 'MASK must be clear', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -339,3 +387,3 @@ | ||
else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; | ||
else return this.haveLength(); | ||
else this.haveLength(cb); | ||
} | ||
@@ -346,6 +394,6 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
getPayloadLength16() { | ||
getPayloadLength16(cb) { | ||
if (this._bufferedBytes < 2) { | ||
@@ -357,3 +405,3 @@ this._loop = false; | ||
this._payloadLength = this.consume(2).readUInt16BE(0); | ||
return this.haveLength(); | ||
this.haveLength(cb); | ||
} | ||
@@ -364,6 +412,6 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
getPayloadLength64() { | ||
getPayloadLength64(cb) { | ||
if (this._bufferedBytes < 8) { | ||
@@ -382,4 +430,3 @@ this._loop = false; | ||
if (num > Math.pow(2, 53 - 32) - 1) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -391,6 +438,9 @@ 'Unsupported WebSocket frame: payload length > 2^53 - 1', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); | ||
return this.haveLength(); | ||
this.haveLength(cb); | ||
} | ||
@@ -401,11 +451,10 @@ | ||
* | ||
* @return {(RangeError|undefined)} A possible error | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
haveLength() { | ||
haveLength(cb) { | ||
if (this._payloadLength && this._opcode < 0x08) { | ||
this._totalPayloadLength += this._payloadLength; | ||
if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { | ||
this._loop = false; | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -417,2 +466,5 @@ 'Max payload size exceeded', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -444,3 +496,2 @@ } | ||
* @param {Function} cb Callback | ||
* @return {(Error|RangeError|undefined)} A possible error | ||
* @private | ||
@@ -467,3 +518,6 @@ */ | ||
if (this._opcode > 0x07) return this.controlMessage(data); | ||
if (this._opcode > 0x07) { | ||
this.controlMessage(data, cb); | ||
return; | ||
} | ||
@@ -485,3 +539,3 @@ if (this._compressed) { | ||
return this.dataMessage(); | ||
this.dataMessage(cb); | ||
} | ||
@@ -505,11 +559,12 @@ | ||
if (this._messageLength > this._maxPayload && this._maxPayload > 0) { | ||
return cb( | ||
error( | ||
RangeError, | ||
'Max payload size exceeded', | ||
false, | ||
1009, | ||
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' | ||
) | ||
const error = this.createError( | ||
RangeError, | ||
'Max payload size exceeded', | ||
false, | ||
1009, | ||
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' | ||
); | ||
cb(error); | ||
return; | ||
} | ||
@@ -520,6 +575,4 @@ | ||
const er = this.dataMessage(); | ||
if (er) return cb(er); | ||
this.startLoop(cb); | ||
this.dataMessage(cb); | ||
if (this._state === GET_INFO) this.startLoop(cb); | ||
}); | ||
@@ -531,46 +584,74 @@ } | ||
* | ||
* @return {(Error|undefined)} A possible error | ||
* @param {Function} cb Callback | ||
* @private | ||
*/ | ||
dataMessage() { | ||
if (this._fin) { | ||
const messageLength = this._messageLength; | ||
const fragments = this._fragments; | ||
dataMessage(cb) { | ||
if (!this._fin) { | ||
this._state = GET_INFO; | ||
return; | ||
} | ||
this._totalPayloadLength = 0; | ||
this._messageLength = 0; | ||
this._fragmented = 0; | ||
this._fragments = []; | ||
const messageLength = this._messageLength; | ||
const fragments = this._fragments; | ||
if (this._opcode === 2) { | ||
let data; | ||
this._totalPayloadLength = 0; | ||
this._messageLength = 0; | ||
this._fragmented = 0; | ||
this._fragments = []; | ||
if (this._binaryType === 'nodebuffer') { | ||
data = concat(fragments, messageLength); | ||
} else if (this._binaryType === 'arraybuffer') { | ||
data = toArrayBuffer(concat(fragments, messageLength)); | ||
} else { | ||
data = fragments; | ||
} | ||
if (this._opcode === 2) { | ||
let data; | ||
if (this._binaryType === 'nodebuffer') { | ||
data = concat(fragments, messageLength); | ||
} else if (this._binaryType === 'arraybuffer') { | ||
data = toArrayBuffer(concat(fragments, messageLength)); | ||
} else { | ||
data = fragments; | ||
} | ||
// | ||
// If the state is `INFLATING`, it means that the frame data was | ||
// decompressed asynchronously, so there is no need to defer the event | ||
// as it will be emitted asynchronously anyway. | ||
// | ||
if (this._state === INFLATING || this._allowSynchronousEvents) { | ||
this.emit('message', data, true); | ||
this._state = GET_INFO; | ||
} else { | ||
const buf = concat(fragments, messageLength); | ||
this._state = DEFER_EVENT; | ||
queueTask(() => { | ||
this.emit('message', data, true); | ||
this._state = GET_INFO; | ||
this.startLoop(cb); | ||
}); | ||
} | ||
} else { | ||
const buf = concat(fragments, messageLength); | ||
if (!this._skipUTF8Validation && !isValidUTF8(buf)) { | ||
this._loop = false; | ||
return error( | ||
Error, | ||
'invalid UTF-8 sequence', | ||
true, | ||
1007, | ||
'WS_ERR_INVALID_UTF8' | ||
); | ||
} | ||
if (!this._skipUTF8Validation && !isValidUTF8(buf)) { | ||
const error = this.createError( | ||
Error, | ||
'invalid UTF-8 sequence', | ||
true, | ||
1007, | ||
'WS_ERR_INVALID_UTF8' | ||
); | ||
cb(error); | ||
return; | ||
} | ||
if (this._state === INFLATING || this._allowSynchronousEvents) { | ||
this.emit('message', buf, false); | ||
this._state = GET_INFO; | ||
} else { | ||
this._state = DEFER_EVENT; | ||
queueTask(() => { | ||
this.emit('message', buf, false); | ||
this._state = GET_INFO; | ||
this.startLoop(cb); | ||
}); | ||
} | ||
} | ||
this._state = GET_INFO; | ||
} | ||
@@ -585,17 +666,8 @@ | ||
*/ | ||
controlMessage(data) { | ||
controlMessage(data, cb) { | ||
if (this._opcode === 0x08) { | ||
this._loop = false; | ||
if (data.length === 0) { | ||
this._loop = false; | ||
this.emit('conclude', 1005, EMPTY_BUFFER); | ||
this.end(); | ||
} else if (data.length === 1) { | ||
return error( | ||
RangeError, | ||
'invalid payload length 1', | ||
true, | ||
1002, | ||
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' | ||
); | ||
} else { | ||
@@ -605,3 +677,3 @@ const code = data.readUInt16BE(0); | ||
if (!isValidStatusCode(code)) { | ||
return error( | ||
const error = this.createError( | ||
RangeError, | ||
@@ -613,8 +685,15 @@ `invalid status code ${code}`, | ||
); | ||
cb(error); | ||
return; | ||
} | ||
const buf = data.slice(2); | ||
const buf = new FastBuffer( | ||
data.buffer, | ||
data.byteOffset + 2, | ||
data.length - 2 | ||
); | ||
if (!this._skipUTF8Validation && !isValidUTF8(buf)) { | ||
return error( | ||
const error = this.createError( | ||
Error, | ||
@@ -626,14 +705,53 @@ 'invalid UTF-8 sequence', | ||
); | ||
cb(error); | ||
return; | ||
} | ||
this._loop = false; | ||
this.emit('conclude', code, buf); | ||
this.end(); | ||
} | ||
} else if (this._opcode === 0x09) { | ||
this.emit('ping', data); | ||
this._state = GET_INFO; | ||
return; | ||
} | ||
if (this._allowSynchronousEvents) { | ||
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); | ||
this._state = GET_INFO; | ||
} else { | ||
this.emit('pong', data); | ||
this._state = DEFER_EVENT; | ||
queueTask(() => { | ||
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); | ||
this._state = GET_INFO; | ||
this.startLoop(cb); | ||
}); | ||
} | ||
} | ||
this._state = GET_INFO; | ||
/** | ||
* Builds an error object. | ||
* | ||
* @param {function(new: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 | ||
* @param {String} errorCode The exposed error code | ||
* @return {(Error|RangeError)} The error | ||
* @private | ||
*/ | ||
createError(ErrorCtor, message, prefix, statusCode, errorCode) { | ||
this._loop = false; | ||
this._errored = true; | ||
const err = new ErrorCtor( | ||
prefix ? `Invalid WebSocket frame: ${message}` : message | ||
); | ||
Error.captureStackTrace(err, this.createError); | ||
err.code = errorCode; | ||
err[kStatusCode] = statusCode; | ||
return err; | ||
} | ||
@@ -645,22 +763,28 @@ } | ||
/** | ||
* Builds an error object. | ||
* A shim for `queueMicrotask()`. | ||
* | ||
* @param {function(new: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 | ||
* @param {String} errorCode The exposed error code | ||
* @return {(Error|RangeError)} The error | ||
* @param {Function} cb Callback | ||
*/ | ||
function queueMicrotaskShim(cb) { | ||
promise.then(cb).catch(throwErrorNextTick); | ||
} | ||
/** | ||
* Throws an error. | ||
* | ||
* @param {Error} err The error to throw | ||
* @private | ||
*/ | ||
function error(ErrorCtor, message, prefix, statusCode, errorCode) { | ||
const err = new ErrorCtor( | ||
prefix ? `Invalid WebSocket frame: ${message}` : message | ||
); | ||
function throwError(err) { | ||
throw err; | ||
} | ||
Error.captureStackTrace(err, error); | ||
err.code = errorCode; | ||
err[kStatusCode] = statusCode; | ||
return err; | ||
/** | ||
* Throws an error in the next tick. | ||
* | ||
* @param {Error} err The error to throw | ||
* @private | ||
*/ | ||
function throwErrorNextTick(err) { | ||
process.nextTick(throwError, err); | ||
} |
@@ -1,7 +0,6 @@ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ | ||
'use strict'; | ||
const net = require('net'); | ||
const tls = require('tls'); | ||
const { Duplex } = require('stream'); | ||
const { randomFillSync } = require('crypto'); | ||
@@ -14,2 +13,3 @@ | ||
const kByteLength = Symbol('kByteLength'); | ||
const maskBuffer = Buffer.alloc(4); | ||
@@ -24,3 +24,3 @@ | ||
* | ||
* @param {(net.Socket|tls.Socket)} socket The connection socket | ||
* @param {Duplex} socket The connection socket | ||
* @param {Object} [extensions] An object containing the negotiated extensions | ||
@@ -51,3 +51,3 @@ * @param {Function} [generateMask] The function used to generate the masking | ||
* | ||
* @param {Buffer} data The data to frame | ||
* @param {(Buffer|String)} data The data to frame | ||
* @param {Object} options Options object | ||
@@ -67,3 +67,3 @@ * @param {Boolean} [options.fin=false] Specifies whether or not to set the | ||
* RSV1 bit | ||
* @return {Buffer[]} The framed data as a list of `Buffer` instances | ||
* @return {(Buffer|String)[]} The framed data | ||
* @public | ||
@@ -87,13 +87,28 @@ */ | ||
skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; | ||
if (options.readOnly && !skipMasking) merge = true; | ||
offset = 6; | ||
} | ||
let payloadLength = data.length; | ||
let dataLength; | ||
if (data.length >= 65536) { | ||
if (typeof data === 'string') { | ||
if ( | ||
(!options.mask || skipMasking) && | ||
options[kByteLength] !== undefined | ||
) { | ||
dataLength = options[kByteLength]; | ||
} else { | ||
data = Buffer.from(data); | ||
dataLength = data.length; | ||
} | ||
} else { | ||
dataLength = data.length; | ||
merge = options.mask && options.readOnly && !skipMasking; | ||
} | ||
let payloadLength = dataLength; | ||
if (dataLength >= 65536) { | ||
offset += 8; | ||
payloadLength = 127; | ||
} else if (data.length > 125) { | ||
} else if (dataLength > 125) { | ||
offset += 2; | ||
@@ -103,3 +118,3 @@ payloadLength = 126; | ||
const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); | ||
const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); | ||
@@ -112,6 +127,6 @@ target[0] = options.fin ? options.opcode | 0x80 : options.opcode; | ||
if (payloadLength === 126) { | ||
target.writeUInt16BE(data.length, 2); | ||
target.writeUInt16BE(dataLength, 2); | ||
} else if (payloadLength === 127) { | ||
target[2] = target[3] = 0; | ||
target.writeUIntBE(data.length, 4, 6); | ||
target.writeUIntBE(dataLength, 4, 6); | ||
} | ||
@@ -130,7 +145,7 @@ | ||
if (merge) { | ||
applyMask(data, mask, target, offset, data.length); | ||
applyMask(data, mask, target, offset, dataLength); | ||
return [target]; | ||
} | ||
applyMask(data, mask, data, 0, data.length); | ||
applyMask(data, mask, data, 0, dataLength); | ||
return [target, data]; | ||
@@ -175,6 +190,17 @@ } | ||
const options = { | ||
[kByteLength]: buf.length, | ||
fin: true, | ||
generateMask: this._generateMask, | ||
mask, | ||
maskBuffer: this._maskBuffer, | ||
opcode: 0x08, | ||
readOnly: false, | ||
rsv1: false | ||
}; | ||
if (this._deflating) { | ||
this.enqueue([this.doClose, buf, mask, cb]); | ||
this.enqueue([this.dispatch, buf, false, options, cb]); | ||
} else { | ||
this.doClose(buf, mask, cb); | ||
this.sendFrame(Sender.frame(buf, options), cb); | ||
} | ||
@@ -184,25 +210,2 @@ } | ||
/** | ||
* Frames and sends a close message. | ||
* | ||
* @param {Buffer} data The message to send | ||
* @param {Boolean} [mask=false] 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, | ||
maskBuffer: this._maskBuffer, | ||
generateMask: this._generateMask, | ||
readOnly: false | ||
}), | ||
cb | ||
); | ||
} | ||
/** | ||
* Sends a ping message to the other peer. | ||
@@ -216,12 +219,33 @@ * | ||
ping(data, mask, cb) { | ||
const buf = toBuffer(data); | ||
let byteLength; | ||
let readOnly; | ||
if (buf.length > 125) { | ||
if (typeof data === 'string') { | ||
byteLength = Buffer.byteLength(data); | ||
readOnly = false; | ||
} else { | ||
data = toBuffer(data); | ||
byteLength = data.length; | ||
readOnly = toBuffer.readOnly; | ||
} | ||
if (byteLength > 125) { | ||
throw new RangeError('The data size must not be greater than 125 bytes'); | ||
} | ||
const options = { | ||
[kByteLength]: byteLength, | ||
fin: true, | ||
generateMask: this._generateMask, | ||
mask, | ||
maskBuffer: this._maskBuffer, | ||
opcode: 0x09, | ||
readOnly, | ||
rsv1: false | ||
}; | ||
if (this._deflating) { | ||
this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); | ||
this.enqueue([this.dispatch, data, false, options, cb]); | ||
} else { | ||
this.doPing(buf, mask, toBuffer.readOnly, cb); | ||
this.sendFrame(Sender.frame(data, options), cb); | ||
} | ||
@@ -231,26 +255,2 @@ } | ||
/** | ||
* Frames and sends a ping message. | ||
* | ||
* @param {Buffer} data The message to send | ||
* @param {Boolean} [mask=false] Specifies whether or not to mask `data` | ||
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified | ||
* @param {Function} [cb] Callback | ||
* @private | ||
*/ | ||
doPing(data, mask, readOnly, cb) { | ||
this.sendFrame( | ||
Sender.frame(data, { | ||
fin: true, | ||
rsv1: false, | ||
opcode: 0x09, | ||
mask, | ||
maskBuffer: this._maskBuffer, | ||
generateMask: this._generateMask, | ||
readOnly | ||
}), | ||
cb | ||
); | ||
} | ||
/** | ||
* Sends a pong message to the other peer. | ||
@@ -264,12 +264,33 @@ * | ||
pong(data, mask, cb) { | ||
const buf = toBuffer(data); | ||
let byteLength; | ||
let readOnly; | ||
if (buf.length > 125) { | ||
if (typeof data === 'string') { | ||
byteLength = Buffer.byteLength(data); | ||
readOnly = false; | ||
} else { | ||
data = toBuffer(data); | ||
byteLength = data.length; | ||
readOnly = toBuffer.readOnly; | ||
} | ||
if (byteLength > 125) { | ||
throw new RangeError('The data size must not be greater than 125 bytes'); | ||
} | ||
const options = { | ||
[kByteLength]: byteLength, | ||
fin: true, | ||
generateMask: this._generateMask, | ||
mask, | ||
maskBuffer: this._maskBuffer, | ||
opcode: 0x0a, | ||
readOnly, | ||
rsv1: false | ||
}; | ||
if (this._deflating) { | ||
this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); | ||
this.enqueue([this.dispatch, data, false, options, cb]); | ||
} else { | ||
this.doPong(buf, mask, toBuffer.readOnly, cb); | ||
this.sendFrame(Sender.frame(data, options), cb); | ||
} | ||
@@ -279,26 +300,2 @@ } | ||
/** | ||
* Frames and sends a pong message. | ||
* | ||
* @param {Buffer} data The message to send | ||
* @param {Boolean} [mask=false] Specifies whether or not to mask `data` | ||
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified | ||
* @param {Function} [cb] Callback | ||
* @private | ||
*/ | ||
doPong(data, mask, readOnly, cb) { | ||
this.sendFrame( | ||
Sender.frame(data, { | ||
fin: true, | ||
rsv1: false, | ||
opcode: 0x0a, | ||
mask, | ||
maskBuffer: this._maskBuffer, | ||
generateMask: this._generateMask, | ||
readOnly | ||
}), | ||
cb | ||
); | ||
} | ||
/** | ||
* Sends a data message to the other peer. | ||
@@ -320,3 +317,2 @@ * | ||
send(data, options, cb) { | ||
const buf = toBuffer(data); | ||
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; | ||
@@ -326,2 +322,14 @@ let opcode = options.binary ? 2 : 1; | ||
let byteLength; | ||
let readOnly; | ||
if (typeof data === 'string') { | ||
byteLength = Buffer.byteLength(data); | ||
readOnly = false; | ||
} else { | ||
data = toBuffer(data); | ||
byteLength = data.length; | ||
readOnly = toBuffer.readOnly; | ||
} | ||
if (this._firstFragment) { | ||
@@ -338,3 +346,3 @@ this._firstFragment = false; | ||
) { | ||
rsv1 = buf.length >= perMessageDeflate._threshold; | ||
rsv1 = byteLength >= perMessageDeflate._threshold; | ||
} | ||
@@ -351,26 +359,28 @@ this._compress = rsv1; | ||
const opts = { | ||
[kByteLength]: byteLength, | ||
fin: options.fin, | ||
rsv1, | ||
opcode, | ||
generateMask: this._generateMask, | ||
mask: options.mask, | ||
maskBuffer: this._maskBuffer, | ||
generateMask: this._generateMask, | ||
readOnly: toBuffer.readOnly | ||
opcode, | ||
readOnly, | ||
rsv1 | ||
}; | ||
if (this._deflating) { | ||
this.enqueue([this.dispatch, buf, this._compress, opts, cb]); | ||
this.enqueue([this.dispatch, data, this._compress, opts, cb]); | ||
} else { | ||
this.dispatch(buf, this._compress, opts, cb); | ||
this.dispatch(data, this._compress, opts, cb); | ||
} | ||
} else { | ||
this.sendFrame( | ||
Sender.frame(buf, { | ||
Sender.frame(data, { | ||
[kByteLength]: byteLength, | ||
fin: options.fin, | ||
rsv1: false, | ||
opcode, | ||
generateMask: this._generateMask, | ||
mask: options.mask, | ||
maskBuffer: this._maskBuffer, | ||
generateMask: this._generateMask, | ||
readOnly: toBuffer.readOnly | ||
opcode, | ||
readOnly, | ||
rsv1: false | ||
}), | ||
@@ -383,9 +393,8 @@ cb | ||
/** | ||
* Dispatches a data message. | ||
* Dispatches a message. | ||
* | ||
* @param {Buffer} data The message to send | ||
* @param {(Buffer|String)} data The message to send | ||
* @param {Boolean} [compress=false] Specifies whether or not to compress | ||
* `data` | ||
* @param {Object} options Options object | ||
* @param {Number} options.opcode The opcode | ||
* @param {Boolean} [options.fin=false] Specifies whether or not to set the | ||
@@ -399,2 +408,3 @@ * FIN bit | ||
* key | ||
* @param {Number} options.opcode The opcode | ||
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be | ||
@@ -415,3 +425,3 @@ * modified | ||
this._bufferedBytes += data.length; | ||
this._bufferedBytes += options[kByteLength]; | ||
this._deflating = true; | ||
@@ -427,3 +437,4 @@ perMessageDeflate.compress(data, options.fin, (_, buf) => { | ||
for (let i = 0; i < this._queue.length; i++) { | ||
const callback = this._queue[i][4]; | ||
const params = this._queue[i]; | ||
const callback = params[params.length - 1]; | ||
@@ -436,3 +447,3 @@ if (typeof callback === 'function') callback(err); | ||
this._bufferedBytes -= data.length; | ||
this._bufferedBytes -= options[kByteLength]; | ||
this._deflating = false; | ||
@@ -454,3 +465,3 @@ options.readOnly = false; | ||
this._bufferedBytes -= params[1].length; | ||
this._bufferedBytes -= params[3][kByteLength]; | ||
Reflect.apply(params[0], this, params.slice(1)); | ||
@@ -467,3 +478,3 @@ } | ||
enqueue(params) { | ||
this._bufferedBytes += params[1].length; | ||
this._bufferedBytes += params[3][kByteLength]; | ||
this._queue.push(params); | ||
@@ -470,0 +481,0 @@ } |
'use strict'; | ||
const { isUtf8 } = require('buffer'); | ||
// | ||
@@ -108,18 +110,22 @@ // Allowed token characters: | ||
try { | ||
const isValidUTF8 = require('utf-8-validate'); | ||
module.exports = { | ||
isValidStatusCode, | ||
isValidUTF8: _isValidUTF8, | ||
tokenChars | ||
}; | ||
module.exports = { | ||
isValidStatusCode, | ||
isValidUTF8(buf) { | ||
return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); | ||
}, | ||
tokenChars | ||
if (isUtf8) { | ||
module.exports.isValidUTF8 = function (buf) { | ||
return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); | ||
}; | ||
} catch (e) /* istanbul ignore next */ { | ||
module.exports = { | ||
isValidStatusCode, | ||
isValidUTF8: _isValidUTF8, | ||
tokenChars | ||
}; | ||
} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { | ||
try { | ||
const isValidUTF8 = require('utf-8-validate'); | ||
module.exports.isValidUTF8 = function (buf) { | ||
return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); | ||
}; | ||
} catch (e) { | ||
// Continue regardless of the error. | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$" }] */ | ||
@@ -7,5 +7,3 @@ 'use strict'; | ||
const http = require('http'); | ||
const https = require('https'); | ||
const net = require('net'); | ||
const tls = require('tls'); | ||
const { Duplex } = require('stream'); | ||
const { createHash } = require('crypto'); | ||
@@ -35,2 +33,7 @@ | ||
* @param {Object} options Configuration options | ||
* @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether | ||
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted | ||
* multiple times in the same tick | ||
* @param {Boolean} [options.autoPong=true] Specifies whether or not to | ||
* automatically send a pong in response to a ping | ||
* @param {Number} [options.backlog=511] The maximum length of the queue of | ||
@@ -54,2 +57,4 @@ * pending connections | ||
* @param {Function} [options.verifyClient] A hook to reject connections | ||
* @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` | ||
* class to use. It must be the `WebSocket` class or class that extends it | ||
* @param {Function} [callback] A listener for the `listening` event | ||
@@ -61,2 +66,4 @@ */ | ||
options = { | ||
allowSynchronousEvents: false, | ||
autoPong: true, | ||
maxPayload: 100 * 1024 * 1024, | ||
@@ -74,2 +81,3 @@ skipUTF8Validation: false, | ||
port: null, | ||
WebSocket, | ||
...options | ||
@@ -226,4 +234,3 @@ }; | ||
* @param {http.IncomingMessage} req The request object | ||
* @param {(net.Socket|tls.Socket)} socket The network socket between the | ||
* server and client | ||
* @param {Duplex} socket The network socket between the server and client | ||
* @param {Buffer} head The first packet of the upgraded stream | ||
@@ -236,19 +243,34 @@ * @param {Function} cb Callback | ||
const key = | ||
req.headers['sec-websocket-key'] !== undefined | ||
? req.headers['sec-websocket-key'] | ||
: false; | ||
const key = req.headers['sec-websocket-key']; | ||
const version = +req.headers['sec-websocket-version']; | ||
if ( | ||
req.method !== 'GET' || | ||
req.headers.upgrade.toLowerCase() !== 'websocket' || | ||
!key || | ||
!keyRegex.test(key) || | ||
(version !== 8 && version !== 13) || | ||
!this.shouldHandle(req) | ||
) { | ||
return abortHandshake(socket, 400); | ||
if (req.method !== 'GET') { | ||
const message = 'Invalid HTTP method'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); | ||
return; | ||
} | ||
if (req.headers.upgrade.toLowerCase() !== 'websocket') { | ||
const message = 'Invalid Upgrade header'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); | ||
return; | ||
} | ||
if (!key || !keyRegex.test(key)) { | ||
const message = 'Missing or invalid Sec-WebSocket-Key header'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); | ||
return; | ||
} | ||
if (version !== 8 && version !== 13) { | ||
const message = 'Missing or invalid Sec-WebSocket-Version header'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); | ||
return; | ||
} | ||
if (!this.shouldHandle(req)) { | ||
abortHandshake(socket, 400); | ||
return; | ||
} | ||
const secWebSocketProtocol = req.headers['sec-websocket-protocol']; | ||
@@ -261,3 +283,5 @@ let protocols = new Set(); | ||
} catch (err) { | ||
return abortHandshake(socket, 400); | ||
const message = 'Invalid Sec-WebSocket-Protocol header'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); | ||
return; | ||
} | ||
@@ -287,3 +311,6 @@ } | ||
} catch (err) { | ||
return abortHandshake(socket, 400); | ||
const message = | ||
'Invalid or unacceptable Sec-WebSocket-Extensions header'; | ||
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); | ||
return; | ||
} | ||
@@ -335,4 +362,3 @@ } | ||
* @param {http.IncomingMessage} req The request object | ||
* @param {(net.Socket|tls.Socket)} socket The network socket between the | ||
* server and client | ||
* @param {Duplex} socket The network socket between the server and client | ||
* @param {Buffer} head The first packet of the upgraded stream | ||
@@ -369,3 +395,3 @@ * @param {Function} cb Callback | ||
const ws = new WebSocket(null); | ||
const ws = new this.options.WebSocket(null, undefined, this.options); | ||
@@ -404,2 +430,3 @@ if (protocols.size) { | ||
ws.setSocket(socket, head, { | ||
allowSynchronousEvents: this.options.allowSynchronousEvents, | ||
maxPayload: this.options.maxPayload, | ||
@@ -458,3 +485,3 @@ skipUTF8Validation: this.options.skipUTF8Validation | ||
/** | ||
* Handle premature socket errors. | ||
* Handle socket errors. | ||
* | ||
@@ -470,3 +497,3 @@ * @private | ||
* | ||
* @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request | ||
* @param {Duplex} socket The socket of the upgrade request | ||
* @param {Number} code The HTTP response status code | ||
@@ -478,23 +505,50 @@ * @param {String} [message] The HTTP response body | ||
function abortHandshake(socket, code, message, headers) { | ||
if (socket.writable) { | ||
message = message || http.STATUS_CODES[code]; | ||
headers = { | ||
Connection: 'close', | ||
'Content-Type': 'text/html', | ||
'Content-Length': Buffer.byteLength(message), | ||
...headers | ||
}; | ||
// | ||
// The socket is writable unless the user destroyed or ended it before calling | ||
// `server.handleUpgrade()` or in the `verifyClient` function, which is a user | ||
// error. Handling this does not make much sense as the worst that can happen | ||
// is that some of the data written by the user might be discarded due to the | ||
// call to `socket.end()` below, which triggers an `'error'` event that in | ||
// turn causes the socket to be destroyed. | ||
// | ||
message = message || http.STATUS_CODES[code]; | ||
headers = { | ||
Connection: 'close', | ||
'Content-Type': 'text/html', | ||
'Content-Length': Buffer.byteLength(message), | ||
...headers | ||
}; | ||
socket.write( | ||
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + | ||
Object.keys(headers) | ||
.map((h) => `${h}: ${headers[h]}`) | ||
.join('\r\n') + | ||
'\r\n\r\n' + | ||
message | ||
); | ||
} | ||
socket.once('finish', socket.destroy); | ||
socket.removeListener('error', socketOnError); | ||
socket.destroy(); | ||
socket.end( | ||
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + | ||
Object.keys(headers) | ||
.map((h) => `${h}: ${headers[h]}`) | ||
.join('\r\n') + | ||
'\r\n\r\n' + | ||
message | ||
); | ||
} | ||
/** | ||
* Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least | ||
* one listener for it, otherwise call `abortHandshake()`. | ||
* | ||
* @param {WebSocketServer} server The WebSocket server | ||
* @param {http.IncomingMessage} req The request object | ||
* @param {Duplex} socket The socket of the upgrade request | ||
* @param {Number} code The HTTP response status code | ||
* @param {String} message The HTTP response body | ||
* @private | ||
*/ | ||
function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) { | ||
if (server.listenerCount('wsClientError')) { | ||
const err = new Error(message); | ||
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); | ||
server.emit('wsClientError', err, socket, req); | ||
} else { | ||
abortHandshake(socket, code, message); | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ | ||
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$" }] */ | ||
@@ -11,3 +11,3 @@ 'use strict'; | ||
const { randomBytes, createHash } = require('crypto'); | ||
const { Readable } = require('stream'); | ||
const { Duplex, Readable } = require('stream'); | ||
const { URL } = require('url'); | ||
@@ -34,6 +34,7 @@ | ||
const closeTimeout = 30 * 1000; | ||
const kAborted = Symbol('kAborted'); | ||
const protocolVersions = [8, 13]; | ||
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; | ||
const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; | ||
const protocolVersions = [8, 13]; | ||
const closeTimeout = 30 * 1000; | ||
@@ -88,2 +89,3 @@ /** | ||
} else { | ||
this._autoPong = options.autoPong; | ||
this._isServer = true; | ||
@@ -194,6 +196,8 @@ } | ||
* | ||
* @param {(net.Socket|tls.Socket)} socket The network socket between the | ||
* server and client | ||
* @param {Duplex} socket The network socket between the server and client | ||
* @param {Buffer} head The first packet of the upgraded stream | ||
* @param {Object} options Options object | ||
* @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether | ||
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted | ||
* multiple times in the same tick | ||
* @param {Function} [options.generateMask] The function used to generate the | ||
@@ -208,2 +212,3 @@ * masking key | ||
const receiver = new Receiver({ | ||
allowSynchronousEvents: options.allowSynchronousEvents, | ||
binaryType: this.binaryType, | ||
@@ -230,4 +235,7 @@ extensions: this._extensions, | ||
socket.setTimeout(0); | ||
socket.setNoDelay(); | ||
// | ||
// These methods may not be available if `socket` is just a `Duplex`. | ||
// | ||
if (socket.setTimeout) socket.setTimeout(0); | ||
if (socket.setNoDelay) socket.setNoDelay(); | ||
@@ -290,3 +298,4 @@ if (head.length > 0) socket.unshift(head); | ||
const msg = 'WebSocket was closed before the connection was established'; | ||
return abortHandshake(this, this._req, msg); | ||
abortHandshake(this, this._req, msg); | ||
return; | ||
} | ||
@@ -486,3 +495,4 @@ | ||
const msg = 'WebSocket was closed before the connection was established'; | ||
return abortHandshake(this, this._req, msg); | ||
abortHandshake(this, this._req, msg); | ||
return; | ||
} | ||
@@ -624,2 +634,9 @@ | ||
* @param {Object} [options] Connection options | ||
* @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether any | ||
* of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple | ||
* times in the same tick | ||
* @param {Boolean} [options.autoPong=true] Specifies whether or not to | ||
* automatically send a pong in response to a ping | ||
* @param {Function} [options.finishRequest] A function which can be used to | ||
* customize the headers of each http request before it is sent | ||
* @param {Boolean} [options.followRedirects=false] Whether or not to follow | ||
@@ -647,2 +664,4 @@ * redirects | ||
const opts = { | ||
allowSynchronousEvents: false, | ||
autoPong: true, | ||
protocolVersion: protocolVersions[1], | ||
@@ -660,3 +679,3 @@ maxPayload: 100 * 1024 * 1024, | ||
timeout: undefined, | ||
method: undefined, | ||
method: 'GET', | ||
host: undefined, | ||
@@ -667,2 +686,4 @@ path: undefined, | ||
websocket._autoPong = opts.autoPong; | ||
if (!protocolVersions.includes(opts.protocolVersion)) { | ||
@@ -679,3 +700,2 @@ throw new RangeError( | ||
parsedUrl = address; | ||
websocket._url = address.href; | ||
} else { | ||
@@ -687,21 +707,28 @@ try { | ||
} | ||
} | ||
websocket._url = address; | ||
if (parsedUrl.protocol === 'http:') { | ||
parsedUrl.protocol = 'ws:'; | ||
} else if (parsedUrl.protocol === 'https:') { | ||
parsedUrl.protocol = 'wss:'; | ||
} | ||
websocket._url = parsedUrl.href; | ||
const isSecure = parsedUrl.protocol === 'wss:'; | ||
const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; | ||
let invalidURLMessage; | ||
const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; | ||
let invalidUrlMessage; | ||
if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { | ||
invalidURLMessage = | ||
'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; | ||
} else if (isUnixSocket && !parsedUrl.pathname) { | ||
invalidURLMessage = "The URL's pathname is empty"; | ||
if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { | ||
invalidUrlMessage = | ||
'The URL\'s protocol must be one of "ws:", "wss:", ' + | ||
'"http:", "https", or "ws+unix:"'; | ||
} else if (isIpcUrl && !parsedUrl.pathname) { | ||
invalidUrlMessage = "The URL's pathname is empty"; | ||
} else if (parsedUrl.hash) { | ||
invalidURLMessage = 'The URL contains a fragment identifier'; | ||
invalidUrlMessage = 'The URL contains a fragment identifier'; | ||
} | ||
if (invalidURLMessage) { | ||
const err = new SyntaxError(invalidURLMessage); | ||
if (invalidUrlMessage) { | ||
const err = new SyntaxError(invalidUrlMessage); | ||
@@ -718,3 +745,3 @@ if (websocket._redirects === 0) { | ||
const key = randomBytes(16).toString('base64'); | ||
const get = isSecure ? https.get : http.get; | ||
const request = isSecure ? https.request : http.request; | ||
const protocolSet = new Set(); | ||
@@ -730,7 +757,7 @@ let perMessageDeflate; | ||
opts.headers = { | ||
...opts.headers, | ||
'Sec-WebSocket-Version': opts.protocolVersion, | ||
'Sec-WebSocket-Key': key, | ||
Connection: 'Upgrade', | ||
Upgrade: 'websocket', | ||
...opts.headers | ||
Upgrade: 'websocket' | ||
}; | ||
@@ -778,3 +805,3 @@ opts.path = parsedUrl.pathname + parsedUrl.search; | ||
if (isUnixSocket) { | ||
if (isIpcUrl) { | ||
const parts = opts.path.split(':'); | ||
@@ -786,4 +813,76 @@ | ||
let req = (websocket._req = get(opts)); | ||
let req; | ||
if (opts.followRedirects) { | ||
if (websocket._redirects === 0) { | ||
websocket._originalIpc = isIpcUrl; | ||
websocket._originalSecure = isSecure; | ||
websocket._originalHostOrSocketPath = isIpcUrl | ||
? opts.socketPath | ||
: parsedUrl.host; | ||
const headers = options && options.headers; | ||
// | ||
// Shallow copy the user provided options so that headers can be changed | ||
// without mutating the original object. | ||
// | ||
options = { ...options, headers: {} }; | ||
if (headers) { | ||
for (const [key, value] of Object.entries(headers)) { | ||
options.headers[key.toLowerCase()] = value; | ||
} | ||
} | ||
} else if (websocket.listenerCount('redirect') === 0) { | ||
const isSameHost = isIpcUrl | ||
? websocket._originalIpc | ||
? opts.socketPath === websocket._originalHostOrSocketPath | ||
: false | ||
: websocket._originalIpc | ||
? false | ||
: parsedUrl.host === websocket._originalHostOrSocketPath; | ||
if (!isSameHost || (websocket._originalSecure && !isSecure)) { | ||
// | ||
// Match curl 7.77.0 behavior and drop the following headers. These | ||
// headers are also dropped when following a redirect to a subdomain. | ||
// | ||
delete opts.headers.authorization; | ||
delete opts.headers.cookie; | ||
if (!isSameHost) delete opts.headers.host; | ||
opts.auth = undefined; | ||
} | ||
} | ||
// | ||
// Match curl 7.77.0 behavior and make the first `Authorization` header win. | ||
// If the `Authorization` header is set, then there is nothing to do as it | ||
// will take precedence. | ||
// | ||
if (opts.auth && !options.headers.authorization) { | ||
options.headers.authorization = | ||
'Basic ' + Buffer.from(opts.auth).toString('base64'); | ||
} | ||
req = websocket._req = request(opts); | ||
if (websocket._redirects) { | ||
// | ||
// Unlike what is done for the `'upgrade'` event, no early exit is | ||
// triggered here if the user calls `websocket.close()` or | ||
// `websocket.terminate()` from a listener of the `'redirect'` event. This | ||
// is because the user can also call `request.destroy()` with an error | ||
// before calling `websocket.close()` or `websocket.terminate()` and this | ||
// would result in an error being emitted on the `request` object with no | ||
// `'error'` event listeners attached. | ||
// | ||
websocket.emit('redirect', websocket.url, req); | ||
} | ||
} else { | ||
req = websocket._req = request(opts); | ||
} | ||
if (opts.timeout) { | ||
@@ -796,3 +895,3 @@ req.on('timeout', () => { | ||
req.on('error', (err) => { | ||
if (req === null || req.aborted) return; | ||
if (req === null || req[kAborted]) return; | ||
@@ -844,4 +943,4 @@ req = websocket._req = null; | ||
// | ||
// The user may have closed the connection from a listener of the `upgrade` | ||
// event. | ||
// The user may have closed the connection from a listener of the | ||
// `'upgrade'` event. | ||
// | ||
@@ -852,2 +951,7 @@ if (websocket.readyState !== WebSocket.CONNECTING) return; | ||
if (res.headers.upgrade.toLowerCase() !== 'websocket') { | ||
abortHandshake(websocket, socket, 'Invalid Upgrade header'); | ||
return; | ||
} | ||
const digest = createHash('sha1') | ||
@@ -927,2 +1031,3 @@ .update(key + GUID) | ||
websocket.setSocket(socket, head, { | ||
allowSynchronousEvents: opts.allowSynchronousEvents, | ||
generateMask: opts.generateMask, | ||
@@ -933,6 +1038,12 @@ maxPayload: opts.maxPayload, | ||
}); | ||
if (opts.finishRequest) { | ||
opts.finishRequest(req, websocket); | ||
} else { | ||
req.end(); | ||
} | ||
} | ||
/** | ||
* Emit the `'error'` and `'close'` event. | ||
* Emit the `'error'` and `'close'` events. | ||
* | ||
@@ -994,2 +1105,3 @@ * @param {WebSocket} websocket The WebSocket instance | ||
if (stream.setHeader) { | ||
stream[kAborted] = true; | ||
stream.abort(); | ||
@@ -1006,4 +1118,3 @@ | ||
stream.once('abort', websocket.emitClose.bind(websocket)); | ||
websocket.emit('error', err); | ||
process.nextTick(emitErrorAndClose, websocket, err); | ||
} else { | ||
@@ -1044,3 +1155,3 @@ stream.destroy(err); | ||
); | ||
cb(err); | ||
process.nextTick(cb, err); | ||
} | ||
@@ -1136,3 +1247,3 @@ } | ||
websocket.pong(data, !websocket._isServer, NOOP); | ||
if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); | ||
websocket.emit('ping', data); | ||
@@ -1162,3 +1273,3 @@ } | ||
/** | ||
* The listener of the `net.Socket` `'close'` event. | ||
* The listener of the socket `'close'` event. | ||
* | ||
@@ -1214,3 +1325,3 @@ * @private | ||
/** | ||
* The listener of the `net.Socket` `'data'` event. | ||
* The listener of the socket `'data'` event. | ||
* | ||
@@ -1227,3 +1338,3 @@ * @param {Buffer} chunk A chunk of data | ||
/** | ||
* The listener of the `net.Socket` `'end'` event. | ||
* The listener of the socket `'end'` event. | ||
* | ||
@@ -1241,3 +1352,3 @@ * @private | ||
/** | ||
* The listener of the `net.Socket` `'error'` event. | ||
* The listener of the socket `'error'` event. | ||
* | ||
@@ -1244,0 +1355,0 @@ * @private |
{ | ||
"name": "ws", | ||
"version": "8.4.0", | ||
"version": "8.16.0", | ||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", | ||
@@ -15,3 +15,6 @@ "keywords": [ | ||
"bugs": "https://github.com/websockets/ws/issues", | ||
"repository": "websockets/ws", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/websockets/ws.git" | ||
}, | ||
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)", | ||
@@ -21,4 +24,8 @@ "license": "MIT", | ||
"exports": { | ||
"import": "./wrapper.mjs", | ||
"require": "./index.js" | ||
".": { | ||
"browser": "./browser.js", | ||
"import": "./wrapper.mjs", | ||
"require": "./index.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
@@ -42,3 +49,3 @@ "browser": "browser.js", | ||
"bufferutil": "^4.0.1", | ||
"utf-8-validate": "^5.0.2" | ||
"utf-8-validate": ">=5.0.2" | ||
}, | ||
@@ -57,9 +64,9 @@ "peerDependenciesMeta": { | ||
"eslint": "^8.0.0", | ||
"eslint-config-prettier": "^8.1.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-config-prettier": "^9.0.0", | ||
"eslint-plugin-prettier": "^5.0.0", | ||
"mocha": "^8.4.0", | ||
"nyc": "^15.0.0", | ||
"prettier": "^2.0.5", | ||
"utf-8-validate": "^5.0.2" | ||
"prettier": "^3.0.0", | ||
"utf-8-validate": "^6.0.0" | ||
} | ||
} |
102
README.md
# ws: a Node.js WebSocket library | ||
[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) | ||
[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) | ||
[![CI](https://img.shields.io/github/actions/workflow/status/websockets/ws/ci.yml?branch=master&label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) | ||
[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) | ||
@@ -26,2 +26,3 @@ | ||
- [Opt-in for performance](#opt-in-for-performance) | ||
- [Legacy opt-in for performance](#legacy-opt-in-for-performance) | ||
- [API docs](#api-docs) | ||
@@ -37,3 +38,3 @@ - [WebSocket compression](#websocket-compression) | ||
- [Server broadcast](#server-broadcast) | ||
- [echo.websocket.org demo](#echowebsocketorg-demo) | ||
- [Round-trip time](#round-trip-time) | ||
- [Use the Node.js streams API](#use-the-nodejs-streams-api) | ||
@@ -62,13 +63,34 @@ - [Other examples](#other-examples) | ||
There are 2 optional modules that can be installed along side with the ws | ||
module. These modules are binary addons which improve certain operations. | ||
Prebuilt binaries are available for the most popular platforms so you don't | ||
necessarily need to have a C++ compiler installed on your machine. | ||
[bufferutil][] is an optional module that can be installed alongside the ws | ||
module: | ||
- `npm install --save-optional bufferutil`: Allows to efficiently perform | ||
operations such as masking and unmasking the data payload of the WebSocket | ||
frames. | ||
- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a | ||
message contains valid UTF-8. | ||
``` | ||
npm install --save-optional bufferutil | ||
``` | ||
This is a binary addon that improves the performance of certain operations such | ||
as masking and unmasking the data payload of the WebSocket frames. Prebuilt | ||
binaries are available for the most popular platforms, so you don't necessarily | ||
need to have a C++ compiler installed on your machine. | ||
To force ws to not use bufferutil, use the | ||
[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This | ||
can be useful to enhance security in systems where a user can put a package in | ||
the package search path of an application of another user, due to how the | ||
Node.js resolver algorithm works. | ||
#### Legacy opt-in for performance | ||
If you are running on an old version of Node.js (prior to v18.14.0), ws also | ||
supports the [utf-8-validate][] module: | ||
``` | ||
npm install --save-optional utf-8-validate | ||
``` | ||
This contains a binary polyfill for [`buffer.isUtf8()`][]. | ||
To force ws to not use utf-8-validate, use the | ||
[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable. | ||
## API docs | ||
@@ -150,2 +172,4 @@ | ||
ws.on('error', console.error); | ||
ws.on('open', function open() { | ||
@@ -167,2 +191,4 @@ ws.send('something'); | ||
ws.on('error', console.error); | ||
ws.on('open', function open() { | ||
@@ -187,2 +213,4 @@ const array = new Float32Array(5); | ||
wss.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
ws.on('message', function message(data) { | ||
@@ -210,2 +238,4 @@ console.log('received: %s', data); | ||
wss.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
ws.on('message', function message(data) { | ||
@@ -233,2 +263,4 @@ console.log('received: %s', data); | ||
wss1.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
// ... | ||
@@ -238,2 +270,4 @@ }); | ||
wss2.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
// ... | ||
@@ -264,5 +298,9 @@ }); | ||
```js | ||
import WebSocket from 'ws'; | ||
import { createServer } from 'http'; | ||
import { WebSocketServer } from 'ws'; | ||
function onSocketError(err) { | ||
console.error(err); | ||
} | ||
const server = createServer(); | ||
@@ -272,2 +310,4 @@ const wss = new WebSocketServer({ noServer: true }); | ||
wss.on('connection', function connection(ws, request, client) { | ||
ws.on('error', console.error); | ||
ws.on('message', function message(data) { | ||
@@ -279,2 +319,4 @@ console.log(`Received message ${data} from user ${client}`); | ||
server.on('upgrade', function upgrade(request, socket, head) { | ||
socket.on('error', onSocketError); | ||
// This function is not defined on purpose. Implement it with your own logic. | ||
@@ -288,2 +330,4 @@ authenticate(request, function next(err, client) { | ||
socket.removeListener('error', onSocketError); | ||
wss.handleUpgrade(request, socket, head, function done(ws) { | ||
@@ -311,2 +355,4 @@ wss.emit('connection', ws, request, client); | ||
wss.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
ws.on('message', function message(data, isBinary) { | ||
@@ -331,2 +377,4 @@ wss.clients.forEach(function each(client) { | ||
wss.on('connection', function connection(ws) { | ||
ws.on('error', console.error); | ||
ws.on('message', function message(data, isBinary) { | ||
@@ -342,3 +390,3 @@ wss.clients.forEach(function each(client) { | ||
### echo.websocket.org demo | ||
### Round-trip time | ||
@@ -348,6 +396,6 @@ ```js | ||
const ws = new WebSocket('wss://echo.websocket.org/', { | ||
origin: 'https://websocket.org' | ||
}); | ||
const ws = new WebSocket('wss://websocket-echo.com/'); | ||
ws.on('error', console.error); | ||
ws.on('open', function open() { | ||
@@ -363,3 +411,3 @@ console.log('connected'); | ||
ws.on('message', function message(data) { | ||
console.log(`Roundtrip time: ${Date.now() - data} ms`); | ||
console.log(`Round-trip time: ${Date.now() - data} ms`); | ||
@@ -377,8 +425,8 @@ setTimeout(function timeout() { | ||
const ws = new WebSocket('wss://echo.websocket.org/', { | ||
origin: 'https://websocket.org' | ||
}); | ||
const ws = new WebSocket('wss://websocket-echo.com/'); | ||
const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); | ||
duplex.on('error', console.error); | ||
duplex.pipe(process.stdout); | ||
@@ -408,2 +456,4 @@ process.stdin.pipe(duplex); | ||
const ip = req.socket.remoteAddress; | ||
ws.on('error', console.error); | ||
}); | ||
@@ -418,2 +468,4 @@ ``` | ||
const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); | ||
ws.on('error', console.error); | ||
}); | ||
@@ -442,2 +494,3 @@ ``` | ||
ws.isAlive = true; | ||
ws.on('error', console.error); | ||
ws.on('pong', heartbeat); | ||
@@ -482,4 +535,5 @@ }); | ||
const client = new WebSocket('wss://echo.websocket.org/'); | ||
const client = new WebSocket('wss://websocket-echo.com/'); | ||
client.on('error', console.error); | ||
client.on('open', heartbeat); | ||
@@ -505,2 +559,4 @@ client.on('ping', heartbeat); | ||
[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input | ||
[bufferutil]: https://github.com/websockets/bufferutil | ||
[changelog]: https://github.com/websockets/ws/releases | ||
@@ -516,3 +572,3 @@ [client-report]: http://websockets.github.io/ws/autobahn/clients/ | ||
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent | ||
[ws-server-options]: | ||
https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback | ||
[utf-8-validate]: https://github.com/websockets/utf-8-validate | ||
[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback |
Sorry, the diff of this file is not supported yet
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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances 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
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
141298
4064
550
5
2