SPB Git forge

spb/admin-ka

Public
41commits 1branches 0releases
172.9 MBsize
maindefault branch
19 days agolast push
JavaScript 65.5% Python 17.8% CSS 13% HTML 3.7%
14.3 KB · 531 lines javascript
Raw Blame History
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