Comparing version 3.3.1 to 3.3.2
'use strict'; | ||
// | ||
// Allowed token characters: | ||
// | ||
// '!', '#', '$', '%', '&', ''', '*', '+', '-', | ||
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' | ||
// | ||
// tokenChars[32] === 0 // ' ' | ||
// tokenChars[33] === 1 // '!' | ||
// tokenChars[34] === 0 // '"' | ||
// ... | ||
// | ||
const tokenChars = [ | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 | ||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 | ||
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 | ||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 | ||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 | ||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 | ||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 | ||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 | ||
]; | ||
/** | ||
* Parse the `Sec-WebSocket-Extensions` header into an object. | ||
* Adds an offer to the map of extension offers or a parameter to the map of | ||
* parameters. | ||
* | ||
* @param {String} value field value of the header | ||
* @param {Object} dest The map of extension offers or parameters | ||
* @param {String} name The extension or parameter name | ||
* @param {(Object|Boolean|String)} elem The extension parameters or the | ||
* parameter value | ||
* @private | ||
*/ | ||
function push (dest, name, elem) { | ||
if (Object.prototype.hasOwnProperty.call(dest, name)) dest[name].push(elem); | ||
else dest[name] = [elem]; | ||
} | ||
/** | ||
* Parses the `Sec-WebSocket-Extensions` header into an object. | ||
* | ||
* @param {String} header The field value of the header | ||
* @return {Object} The parsed object | ||
* @public | ||
*/ | ||
const parse = (value) => { | ||
value = value || ''; | ||
function parse (header) { | ||
const offers = {}; | ||
const extensions = {}; | ||
if (header === undefined || header === '') return offers; | ||
value.split(',').forEach((v) => { | ||
const params = v.split(';'); | ||
const token = params.shift().trim(); | ||
var params = {}; | ||
var mustUnescape = false; | ||
var isEscaping = false; | ||
var inQuotes = false; | ||
var extensionName; | ||
var paramName; | ||
var start = -1; | ||
var end = -1; | ||
if (extensions[token] === undefined) { | ||
extensions[token] = []; | ||
} else if (!extensions.hasOwnProperty(token)) { | ||
return; | ||
} | ||
for (var i = 0; i < header.length; i++) { | ||
const code = header.charCodeAt(i); | ||
const parsedParams = {}; | ||
if (extensionName === undefined) { | ||
if (end === -1 && tokenChars[code] === 1) { | ||
if (start === -1) start = i; | ||
} else if (code === 0x20/* ' ' */|| code === 0x09/* '\t' */) { | ||
if (end === -1 && start !== -1) end = i; | ||
} else if (code === 0x3b/* ';' */ || code === 0x2c/* ',' */) { | ||
if (start === -1) throw new Error(`unexpected character at index ${i}`); | ||
params.forEach((param) => { | ||
const parts = param.trim().split('='); | ||
const key = parts[0]; | ||
var value = parts[1]; | ||
if (end === -1) end = i; | ||
const name = header.slice(start, end); | ||
if (code === 0x2c) { | ||
push(offers, name, params); | ||
params = {}; | ||
} else { | ||
extensionName = name; | ||
} | ||
if (value === undefined) { | ||
value = true; | ||
start = end = -1; | ||
} else { | ||
// unquote value | ||
if (value[0] === '"') { | ||
value = value.slice(1); | ||
throw new Error(`unexpected character at index ${i}`); | ||
} | ||
} else if (paramName === undefined) { | ||
if (end === -1 && tokenChars[code] === 1) { | ||
if (start === -1) start = i; | ||
} else if (code === 0x20 || code === 0x09) { | ||
if (end === -1 && start !== -1) end = i; | ||
} else if (code === 0x3b || code === 0x2c) { | ||
if (start === -1) throw new Error(`unexpected character at index ${i}`); | ||
if (end === -1) end = i; | ||
push(params, header.slice(start, end), true); | ||
if (code === 0x2c) { | ||
push(offers, extensionName, params); | ||
params = {}; | ||
extensionName = undefined; | ||
} | ||
if (value[value.length - 1] === '"') { | ||
value = value.slice(0, value.length - 1); | ||
start = end = -1; | ||
} else if (code === 0x3d/* '=' */&& start !== -1 && end === -1) { | ||
paramName = header.slice(start, i); | ||
start = end = -1; | ||
} else { | ||
throw new Error(`unexpected character at index ${i}`); | ||
} | ||
} else { | ||
// | ||
// The value of a quoted-string after unescaping must conform to the | ||
// token ABNF, so only token characters are valid. | ||
// Ref: https://tools.ietf.org/html/rfc6455#section-9.1 | ||
// | ||
if (isEscaping) { | ||
if (tokenChars[code] !== 1) { | ||
throw new Error(`unexpected character at index ${i}`); | ||
} | ||
} | ||
if (start === -1) start = i; | ||
else if (!mustUnescape) mustUnescape = true; | ||
isEscaping = false; | ||
} else if (inQuotes) { | ||
if (tokenChars[code] === 1) { | ||
if (start === -1) start = i; | ||
} else if (code === 0x22/* '"' */ && start !== -1) { | ||
inQuotes = false; | ||
end = i; | ||
} else if (code === 0x5c/* '\' */) { | ||
isEscaping = true; | ||
} else { | ||
throw new Error(`unexpected character at index ${i}`); | ||
} | ||
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { | ||
inQuotes = true; | ||
} else if (end === -1 && tokenChars[code] === 1) { | ||
if (start === -1) start = i; | ||
} else if (start !== -1 && (code === 0x20 || code === 0x09)) { | ||
if (end === -1) end = i; | ||
} else if (code === 0x3b || code === 0x2c) { | ||
if (start === -1) throw new Error(`unexpected character at index ${i}`); | ||
if (parsedParams[key] === undefined) { | ||
parsedParams[key] = [value]; | ||
} else if (parsedParams.hasOwnProperty(key)) { | ||
parsedParams[key].push(value); | ||
if (end === -1) end = i; | ||
var value = header.slice(start, end); | ||
if (mustUnescape) { | ||
value = value.replace(/\\/g, ''); | ||
mustUnescape = false; | ||
} | ||
push(params, paramName, value); | ||
if (code === 0x2c) { | ||
push(offers, extensionName, params); | ||
params = {}; | ||
extensionName = undefined; | ||
} | ||
paramName = undefined; | ||
start = end = -1; | ||
} else { | ||
throw new Error(`unexpected character at index ${i}`); | ||
} | ||
}); | ||
} | ||
} | ||
extensions[token].push(parsedParams); | ||
}); | ||
if (start === -1 || inQuotes) throw new Error('unexpected end of input'); | ||
return extensions; | ||
}; | ||
if (end === -1) end = i; | ||
const token = header.slice(start, end); | ||
if (extensionName === undefined) { | ||
push(offers, token, {}); | ||
} else { | ||
if (paramName === undefined) { | ||
push(params, token, true); | ||
} else if (mustUnescape) { | ||
push(params, paramName, token.replace(/\\/g, '')); | ||
} else { | ||
push(params, paramName, token); | ||
} | ||
push(offers, extensionName, params); | ||
} | ||
return offers; | ||
} | ||
/** | ||
* Serialize a parsed `Sec-WebSocket-Extensions` header to a string. | ||
* Serializes a parsed `Sec-WebSocket-Extensions` header to a string. | ||
* | ||
@@ -64,3 +189,3 @@ * @param {Object} value The object to format | ||
*/ | ||
const format = (value) => { | ||
function format (value) { | ||
return Object.keys(value).map((token) => { | ||
@@ -77,4 +202,4 @@ var paramsList = value[token]; | ||
}).join(', '); | ||
}; | ||
} | ||
module.exports = { format, parse }; |
@@ -22,2 +22,3 @@ 'use strict'; | ||
// | ||
// We limit zlib concurrency, which prevents severe memory fragmentation | ||
@@ -27,10 +28,32 @@ // as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 | ||
// | ||
// Intentionally global; it's the global thread pool that's | ||
// an issue. | ||
// Intentionally global; it's the global thread pool that's an issue. | ||
// | ||
let zlibLimiter; | ||
/** | ||
* Per-message Deflate implementation. | ||
* permessage-deflate implementation. | ||
*/ | ||
class PerMessageDeflate { | ||
/** | ||
* Creates a PerMessageDeflate instance. | ||
* | ||
* @param {Object} options Configuration options | ||
* @param {Boolean} options.serverNoContextTakeover Request/accept disabling | ||
* of server context takeover | ||
* @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge | ||
* disabling of client context takeover | ||
* @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the | ||
* use of a custom server window size | ||
* @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support | ||
* for, or request, a custom client window size | ||
* @param {Number} options.level The value of zlib's `level` param | ||
* @param {Number} options.memLevel The value of zlib's `memLevel` param | ||
* @param {Number} options.threshold Size (in bytes) below which messages | ||
* should not be compressed | ||
* @param {Number} options.concurrencyLimit The number of concurrent calls to | ||
* zlib | ||
* @param {Boolean} isServer Create the instance in either server or client | ||
* mode | ||
* @param {Number} maxPayload The maximum allowed message length | ||
*/ | ||
constructor (options, isServer, maxPayload) { | ||
@@ -56,2 +79,5 @@ this._maxPayload = maxPayload | 0; | ||
/** | ||
* @type {String} | ||
*/ | ||
static get extensionName () { | ||
@@ -201,24 +227,17 @@ return 'permessage-deflate'; | ||
if (this._options.clientNoContextTakeover != null) { | ||
if ( | ||
this._options.clientNoContextTakeover === false && | ||
params.client_no_context_takeover | ||
) { | ||
throw new Error('Invalid value for "client_no_context_takeover"'); | ||
} | ||
if ( | ||
this._options.clientNoContextTakeover === false && | ||
params.client_no_context_takeover | ||
) { | ||
throw new Error('Invalid value for "client_no_context_takeover"'); | ||
} | ||
if (this._options.clientMaxWindowBits != null) { | ||
if ( | ||
this._options.clientMaxWindowBits === false && | ||
params.client_max_window_bits | ||
) { | ||
throw new Error('Invalid value for "client_max_window_bits"'); | ||
} | ||
if ( | ||
typeof this._options.clientMaxWindowBits === 'number' && | ||
if ( | ||
(typeof this._options.clientMaxWindowBits === 'number' && | ||
(!params.client_max_window_bits || | ||
params.client_max_window_bits > this._options.clientMaxWindowBits) | ||
) { | ||
throw new Error('Invalid value for "client_max_window_bits"'); | ||
} | ||
params.client_max_window_bits > this._options.clientMaxWindowBits)) || | ||
(this._options.clientMaxWindowBits === false && | ||
params.client_max_window_bits) | ||
) { | ||
throw new Error('Invalid value for "client_max_window_bits"'); | ||
} | ||
@@ -225,0 +244,0 @@ |
@@ -530,17 +530,4 @@ /*! | ||
const httpObj = isSecure ? https : http; | ||
// | ||
// Prepare extensions. | ||
// | ||
const extensionsOffer = {}; | ||
var perMessageDeflate; | ||
if (options.perMessageDeflate) { | ||
perMessageDeflate = new PerMessageDeflate( | ||
options.perMessageDeflate !== true ? options.perMessageDeflate : {}, | ||
false | ||
); | ||
extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer(); | ||
} | ||
const requestOptions = { | ||
@@ -559,4 +546,10 @@ port: serverUrl.port || (isSecure ? 443 : 80), | ||
if (options.headers) Object.assign(requestOptions.headers, options.headers); | ||
if (Object.keys(extensionsOffer).length) { | ||
requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer); | ||
if (options.perMessageDeflate) { | ||
perMessageDeflate = new PerMessageDeflate( | ||
options.perMessageDeflate !== true ? options.perMessageDeflate : {}, | ||
false | ||
); | ||
requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format({ | ||
[PerMessageDeflate.extensionName]: perMessageDeflate.offer() | ||
}); | ||
} | ||
@@ -697,14 +690,19 @@ if (options.protocol) { | ||
const serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']); | ||
if (perMessageDeflate) { | ||
try { | ||
const serverExtensions = Extensions.parse( | ||
res.headers['sec-websocket-extensions'] | ||
); | ||
if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) { | ||
try { | ||
perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]); | ||
if (serverExtensions[PerMessageDeflate.extensionName]) { | ||
perMessageDeflate.accept( | ||
serverExtensions[PerMessageDeflate.extensionName] | ||
); | ||
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate; | ||
} | ||
} catch (err) { | ||
socket.destroy(); | ||
this.emit('error', new Error('invalid extension parameter')); | ||
this.emit('error', new Error('invalid Sec-WebSocket-Extensions header')); | ||
return this.finalize(true); | ||
} | ||
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate; | ||
} | ||
@@ -711,0 +709,0 @@ |
@@ -93,2 +93,3 @@ /*! | ||
if (options.perMessageDeflate === true) options.perMessageDeflate = {}; | ||
if (options.clientTracking) this.clients = new Set(); | ||
@@ -155,2 +156,3 @@ this.options = options; | ||
const version = +req.headers['sec-websocket-version']; | ||
const extensions = {}; | ||
@@ -165,2 +167,23 @@ if ( | ||
if (this.options.perMessageDeflate) { | ||
const perMessageDeflate = new PerMessageDeflate( | ||
this.options.perMessageDeflate, | ||
true, | ||
this.options.maxPayload | ||
); | ||
try { | ||
const offers = Extensions.parse( | ||
req.headers['sec-websocket-extensions'] | ||
); | ||
if (offers[PerMessageDeflate.extensionName]) { | ||
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); | ||
extensions[PerMessageDeflate.extensionName] = perMessageDeflate; | ||
} | ||
} catch (err) { | ||
return abortConnection(socket, 400); | ||
} | ||
} | ||
var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */); | ||
@@ -192,11 +215,19 @@ | ||
this.completeUpgrade(protocol, version, req, socket, head, cb); | ||
this.completeUpgrade( | ||
protocol, | ||
extensions, | ||
version, | ||
req, | ||
socket, | ||
head, | ||
cb | ||
); | ||
}); | ||
return; | ||
} else if (!this.options.verifyClient(info)) { | ||
return abortConnection(socket, 401); | ||
} | ||
if (!this.options.verifyClient(info)) return abortConnection(socket, 401); | ||
} | ||
this.completeUpgrade(protocol, version, req, socket, head, cb); | ||
this.completeUpgrade(protocol, extensions, version, req, socket, head, cb); | ||
} | ||
@@ -208,2 +239,3 @@ | ||
* @param {String} protocol The chosen subprotocol | ||
* @param {Object} extensions The accepted extensions | ||
* @param {Number} version The WebSocket protocol version | ||
@@ -216,3 +248,3 @@ * @param {http.IncomingMessage} req The request object | ||
*/ | ||
completeUpgrade (protocol, version, req, socket, head, cb) { | ||
completeUpgrade (protocol, extensions, version, req, socket, head, cb) { | ||
// | ||
@@ -235,23 +267,10 @@ // Destroy the socket if the client has already sent a FIN packet. | ||
if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`); | ||
const offer = Extensions.parse(req.headers['sec-websocket-extensions']); | ||
var extensions; | ||
try { | ||
extensions = acceptExtensions(this.options, offer); | ||
} catch (err) { | ||
return abortConnection(socket, 400); | ||
if (extensions[PerMessageDeflate.extensionName]) { | ||
const params = extensions[PerMessageDeflate.extensionName].params; | ||
const value = Extensions.format({ | ||
[PerMessageDeflate.extensionName]: [params] | ||
}); | ||
headers.push(`Sec-WebSocket-Extensions: ${value}`); | ||
} | ||
const props = Object.keys(extensions); | ||
if (props.length) { | ||
const serverExtensions = props.reduce((obj, key) => { | ||
obj[key] = [extensions[key].params]; | ||
return obj; | ||
}, {}); | ||
headers.push(`Sec-WebSocket-Extensions: ${Extensions.format(serverExtensions)}`); | ||
} | ||
// | ||
@@ -262,3 +281,3 @@ // Allow external modification/inspection of handshake headers. | ||
socket.write(headers.concat('', '').join('\r\n')); | ||
socket.write(headers.concat('\r\n').join('\r\n')); | ||
@@ -294,28 +313,2 @@ const client = new WebSocket([socket, head], null, { | ||
/** | ||
* Accept WebSocket extensions. | ||
* | ||
* @param {Object} options The `WebSocketServer` configuration options | ||
* @param {Object} offer The parsed value of the `sec-websocket-extensions` header | ||
* @return {Object} Accepted extensions | ||
* @private | ||
*/ | ||
function acceptExtensions (options, offer) { | ||
const pmd = options.perMessageDeflate; | ||
const extensions = {}; | ||
if (pmd && offer[PerMessageDeflate.extensionName]) { | ||
const perMessageDeflate = new PerMessageDeflate( | ||
pmd !== true ? pmd : {}, | ||
true, | ||
options.maxPayload | ||
); | ||
perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]); | ||
extensions[PerMessageDeflate.extensionName] = perMessageDeflate; | ||
} | ||
return extensions; | ||
} | ||
/** | ||
* Close the connection when preconditions are not fulfilled. | ||
@@ -322,0 +315,0 @@ * |
{ | ||
"name": "ws", | ||
"version": "3.3.1", | ||
"version": "3.3.2", | ||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", | ||
@@ -36,3 +36,3 @@ "keywords": [ | ||
"bufferutil": "~3.0.0", | ||
"eslint": "~4.10.0", | ||
"eslint": "~4.11.0", | ||
"eslint-config-standard": "~10.2.0", | ||
@@ -39,0 +39,0 @@ "eslint-plugin-import": "~2.8.0", |
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
99922
2599