JavaScript 65.5%
Python 17.8%
CSS 13%
HTML 3.7%
1'use strict';23const zlib = require('zlib');45const bufferUtil = require('./buffer-util');6const Limiter = require('./limiter');7const { kStatusCode } = require('./constants');89const FastBuffer = Buffer[Symbol.species];10const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);11const kPerMessageDeflate = Symbol('permessage-deflate');12const kTotalLength = Symbol('total-length');13const kCallback = Symbol('callback');14const kBuffers = Symbol('buffers');15const kError = Symbol('error');1617//18// We limit zlib concurrency, which prevents severe memory fragmentation19// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-25091591320// and https://github.com/websockets/ws/issues/120221//22// Intentionally global; it's the global thread pool that's an issue.23//24let zlibLimiter;2526/**27 * permessage-deflate implementation.28 */29class PerMessageDeflate {30 /**31 * Creates a PerMessageDeflate instance.32 *33 * @param {Object} [options] Configuration options34 * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support35 * for, or request, a custom client window size36 * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/37 * acknowledge disabling of client context takeover38 * @param {Number} [options.concurrencyLimit=10] The number of concurrent39 * calls to zlib40 * @param {Boolean} [options.isServer=false] Create the instance in either41 * server or client mode42 * @param {Number} [options.maxPayload=0] The maximum allowed message length43 * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the44 * use of a custom server window size45 * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept46 * disabling of server context takeover47 * @param {Number} [options.threshold=1024] Size (in bytes) below which48 * messages should not be compressed if context takeover is disabled49 * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on50 * deflate51 * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on52 * inflate53 */54 constructor(options) {55 this._options = options || {};56 this._threshold =57 this._options.threshold !== undefined ? this._options.threshold : 1024;58 this._maxPayload = this._options.maxPayload | 0;59 this._isServer = !!this._options.isServer;60 this._deflate = null;61 this._inflate = null;6263 this.params = null;6465 if (!zlibLimiter) {66 const concurrency =67 this._options.concurrencyLimit !== undefined68 ? this._options.concurrencyLimit69 : 10;70 zlibLimiter = new Limiter(concurrency);71 }72 }7374 /**75 * @type {String}76 */77 static get extensionName() {78 return 'permessage-deflate';79 }8081 /**82 * Create an extension negotiation offer.83 *84 * @return {Object} Extension parameters85 * @public86 */87 offer() {88 const params = {};8990 if (this._options.serverNoContextTakeover) {91 params.server_no_context_takeover = true;92 }93 if (this._options.clientNoContextTakeover) {94 params.client_no_context_takeover = true;95 }96 if (this._options.serverMaxWindowBits) {97 params.server_max_window_bits = this._options.serverMaxWindowBits;98 }99 if (this._options.clientMaxWindowBits) {100 params.client_max_window_bits = this._options.clientMaxWindowBits;101 } else if (this._options.clientMaxWindowBits == null) {102 params.client_max_window_bits = true;103 }104105 return params;106 }107108 /**109 * Accept an extension negotiation offer/response.110 *111 * @param {Array} configurations The extension negotiation offers/reponse112 * @return {Object} Accepted configuration113 * @public114 */115 accept(configurations) {116 configurations = this.normalizeParams(configurations);117118 this.params = this._isServer119 ? this.acceptAsServer(configurations)120 : this.acceptAsClient(configurations);121122 return this.params;123 }124125 /**126 * Releases all resources used by the extension.127 *128 * @public129 */130 cleanup() {131 if (this._inflate) {132 this._inflate.close();133 this._inflate = null;134 }135136 if (this._deflate) {137 const callback = this._deflate[kCallback];138139 this._deflate.close();140 this._deflate = null;141142 if (callback) {143 callback(144 new Error(145 'The deflate stream was closed while data was being processed'146 )147 );148 }149 }150 }151152 /**153 * Accept an extension negotiation offer.154 *155 * @param {Array} offers The extension negotiation offers156 * @return {Object} Accepted configuration157 * @private158 */159 acceptAsServer(offers) {160 const opts = this._options;161 const accepted = offers.find((params) => {162 if (163 (opts.serverNoContextTakeover === false &&164 params.server_no_context_takeover) ||165 (params.server_max_window_bits &&166 (opts.serverMaxWindowBits === false ||167 (typeof opts.serverMaxWindowBits === 'number' &&168 opts.serverMaxWindowBits > params.server_max_window_bits))) ||169 (typeof opts.clientMaxWindowBits === 'number' &&170 (typeof params.client_max_window_bits === 'number'171 ? opts.clientMaxWindowBits > params.client_max_window_bits172 : !params.client_max_window_bits))173 ) {174 return false;175 }176177 return true;178 });179180 if (!accepted) {181 throw new Error('None of the extension offers can be accepted');182 }183184 if (opts.serverNoContextTakeover) {185 accepted.server_no_context_takeover = true;186 }187 if (opts.clientNoContextTakeover) {188 accepted.client_no_context_takeover = true;189 }190 if (typeof opts.serverMaxWindowBits === 'number') {191 accepted.server_max_window_bits = opts.serverMaxWindowBits;192 }193 if (typeof opts.clientMaxWindowBits === 'number') {194 accepted.client_max_window_bits = opts.clientMaxWindowBits;195 } else if (196 accepted.client_max_window_bits === true ||197 opts.clientMaxWindowBits === false198 ) {199 delete accepted.client_max_window_bits;200 }201202 return accepted;203 }204205 /**206 * Accept the extension negotiation response.207 *208 * @param {Array} response The extension negotiation response209 * @return {Object} Accepted configuration210 * @private211 */212 acceptAsClient(response) {213 const params = response[0];214215 if (216 this._options.clientNoContextTakeover === false &&217 params.client_no_context_takeover218 ) {219 throw new Error('Unexpected parameter "client_no_context_takeover"');220 }221222 if (!params.client_max_window_bits) {223 if (typeof this._options.clientMaxWindowBits === 'number') {224 params.client_max_window_bits = this._options.clientMaxWindowBits;225 }226 } else if (227 this._options.clientMaxWindowBits === false ||228 (typeof this._options.clientMaxWindowBits === 'number' &&229 params.client_max_window_bits > this._options.clientMaxWindowBits)230 ) {231 throw new Error(232 'Unexpected or invalid parameter "client_max_window_bits"'233 );234 }235236 return params;237 }238239 /**240 * Normalize parameters.241 *242 * @param {Array} configurations The extension negotiation offers/reponse243 * @return {Array} The offers/response with normalized parameters244 * @private245 */246 normalizeParams(configurations) {247 configurations.forEach((params) => {248 Object.keys(params).forEach((key) => {249 let value = params[key];250251 if (value.length > 1) {252 throw new Error(`Parameter "${key}" must have only a single value`);253 }254255 value = value[0];256257 if (key === 'client_max_window_bits') {258 if (value !== true) {259 const num = +value;260 if (!Number.isInteger(num) || num < 8 || num > 15) {261 throw new TypeError(262 `Invalid value for parameter "${key}": ${value}`263 );264 }265 value = num;266 } else if (!this._isServer) {267 throw new TypeError(268 `Invalid value for parameter "${key}": ${value}`269 );270 }271 } else if (key === 'server_max_window_bits') {272 const num = +value;273 if (!Number.isInteger(num) || num < 8 || num > 15) {274 throw new TypeError(275 `Invalid value for parameter "${key}": ${value}`276 );277 }278 value = num;279 } else if (280 key === 'client_no_context_takeover' ||281 key === 'server_no_context_takeover'282 ) {283 if (value !== true) {284 throw new TypeError(285 `Invalid value for parameter "${key}": ${value}`286 );287 }288 } else {289 throw new Error(`Unknown parameter "${key}"`);290 }291292 params[key] = value;293 });294 });295296 return configurations;297 }298299 /**300 * Decompress data. Concurrency limited.301 *302 * @param {Buffer} data Compressed data303 * @param {Boolean} fin Specifies whether or not this is the last fragment304 * @param {Function} callback Callback305 * @public306 */307 decompress(data, fin, callback) {308 zlibLimiter.add((done) => {309 this._decompress(data, fin, (err, result) => {310 done();311 callback(err, result);312 });313 });314 }315316 /**317 * Compress data. Concurrency limited.318 *319 * @param {(Buffer|String)} data Data to compress320 * @param {Boolean} fin Specifies whether or not this is the last fragment321 * @param {Function} callback Callback322 * @public323 */324 compress(data, fin, callback) {325 zlibLimiter.add((done) => {326 this._compress(data, fin, (err, result) => {327 done();328 callback(err, result);329 });330 });331 }332333 /**334 * Decompress data.335 *336 * @param {Buffer} data Compressed data337 * @param {Boolean} fin Specifies whether or not this is the last fragment338 * @param {Function} callback Callback339 * @private340 */341 _decompress(data, fin, callback) {342 const endpoint = this._isServer ? 'client' : 'server';343344 if (!this._inflate) {345 const key = `${endpoint}_max_window_bits`;346 const windowBits =347 typeof this.params[key] !== 'number'348 ? zlib.Z_DEFAULT_WINDOWBITS349 : this.params[key];350351 this._inflate = zlib.createInflateRaw({352 ...this._options.zlibInflateOptions,353 windowBits354 });355 this._inflate[kPerMessageDeflate] = this;356 this._inflate[kTotalLength] = 0;357 this._inflate[kBuffers] = [];358 this._inflate.on('error', inflateOnError);359 this._inflate.on('data', inflateOnData);360 }361362 this._inflate[kCallback] = callback;363364 this._inflate.write(data);365 if (fin) this._inflate.write(TRAILER);366367 this._inflate.flush(() => {368 const err = this._inflate[kError];369370 if (err) {371 this._inflate.close();372 this._inflate = null;373 callback(err);374 return;375 }376377 const data = bufferUtil.concat(378 this._inflate[kBuffers],379 this._inflate[kTotalLength]380 );381382 if (this._inflate._readableState.endEmitted) {383 this._inflate.close();384 this._inflate = null;385 } else {386 this._inflate[kTotalLength] = 0;387 this._inflate[kBuffers] = [];388389 if (fin && this.params[`${endpoint}_no_context_takeover`]) {390 this._inflate.reset();391 }392 }393394 callback(null, data);395 });396 }397398 /**399 * Compress data.400 *401 * @param {(Buffer|String)} data Data to compress402 * @param {Boolean} fin Specifies whether or not this is the last fragment403 * @param {Function} callback Callback404 * @private405 */406 _compress(data, fin, callback) {407 const endpoint = this._isServer ? 'server' : 'client';408409 if (!this._deflate) {410 const key = `${endpoint}_max_window_bits`;411 const windowBits =412 typeof this.params[key] !== 'number'413 ? zlib.Z_DEFAULT_WINDOWBITS414 : this.params[key];415416 this._deflate = zlib.createDeflateRaw({417 ...this._options.zlibDeflateOptions,418 windowBits419 });420421 this._deflate[kTotalLength] = 0;422 this._deflate[kBuffers] = [];423424 this._deflate.on('data', deflateOnData);425 }426427 this._deflate[kCallback] = callback;428429 this._deflate.write(data);430 this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {431 if (!this._deflate) {432 //433 // The deflate stream was closed while data was being processed.434 //435 return;436 }437438 let data = bufferUtil.concat(439 this._deflate[kBuffers],440 this._deflate[kTotalLength]441 );442443 if (fin) {444 data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4);445 }446447 //448 // Ensure that the callback will not be called again in449 // `PerMessageDeflate#cleanup()`.450 //451 this._deflate[kCallback] = null;452453 this._deflate[kTotalLength] = 0;454 this._deflate[kBuffers] = [];455456 if (fin && this.params[`${endpoint}_no_context_takeover`]) {457 this._deflate.reset();458 }459460 callback(null, data);461 });462 }463}464465module.exports = PerMessageDeflate;466467/**468 * The listener of the `zlib.DeflateRaw` stream `'data'` event.469 *470 * @param {Buffer} chunk A chunk of data471 * @private472 */473function deflateOnData(chunk) {474 this[kBuffers].push(chunk);475 this[kTotalLength] += chunk.length;476}477478/**479 * The listener of the `zlib.InflateRaw` stream `'data'` event.480 *481 * @param {Buffer} chunk A chunk of data482 * @private483 */484function inflateOnData(chunk) {485 this[kTotalLength] += chunk.length;486487 if (488 this[kPerMessageDeflate]._maxPayload < 1 ||489 this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload490 ) {491 this[kBuffers].push(chunk);492 return;493 }494495 this[kError] = new RangeError('Max payload size exceeded');496 this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';497 this[kError][kStatusCode] = 1009;498 this.removeListener('data', inflateOnData);499500 //501 // The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the502 // fact that in Node.js versions prior to 13.10.0, the callback for503 // `zlib.flush()` is not called if `zlib.close()` is used. Utilizing504 // `zlib.reset()` ensures that either the callback is invoked or an error is505 // emitted.506 //507 this.reset();508}509510/**511 * The listener of the `zlib.InflateRaw` stream `'error'` event.512 *513 * @param {Error} err The emitted error514 * @private515 */516function inflateOnError(err) {517 //518 // There is no need to call `Zlib#close()` as the handle is automatically519 // closed when an error is emitted.520 //521 this[kPerMessageDeflate]._inflate = null;522523 if (this[kError]) {524 this[kCallback](this[kError]);525 return;526 }527528 err[kStatusCode] = 1007;529 this[kCallback](err);530}531