| 'use strict'; |
|
|
| const zlib = require('zlib'); |
|
|
| const bufferUtil = require('./buffer-util'); |
| const Limiter = require('./limiter'); |
| const { kStatusCode } = require('./constants'); |
|
|
| const FastBuffer = Buffer[Symbol.species]; |
| const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); |
| const kPerMessageDeflate = Symbol('permessage-deflate'); |
| const kTotalLength = Symbol('total-length'); |
| const kCallback = Symbol('callback'); |
| const kBuffers = Symbol('buffers'); |
| const kError = Symbol('error'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| let zlibLimiter; |
|
|
| |
| |
| |
| class PerMessageDeflate { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor(options) { |
| this._options = options || {}; |
| this._threshold = |
| this._options.threshold !== undefined ? this._options.threshold : 1024; |
| this._maxPayload = this._options.maxPayload | 0; |
| this._isServer = !!this._options.isServer; |
| this._deflate = null; |
| this._inflate = null; |
|
|
| this.params = null; |
|
|
| if (!zlibLimiter) { |
| const concurrency = |
| this._options.concurrencyLimit !== undefined |
| ? this._options.concurrencyLimit |
| : 10; |
| zlibLimiter = new Limiter(concurrency); |
| } |
| } |
|
|
| |
| |
| |
| static get extensionName() { |
| return 'permessage-deflate'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| offer() { |
| const params = {}; |
|
|
| if (this._options.serverNoContextTakeover) { |
| params.server_no_context_takeover = true; |
| } |
| if (this._options.clientNoContextTakeover) { |
| params.client_no_context_takeover = true; |
| } |
| if (this._options.serverMaxWindowBits) { |
| params.server_max_window_bits = this._options.serverMaxWindowBits; |
| } |
| if (this._options.clientMaxWindowBits) { |
| params.client_max_window_bits = this._options.clientMaxWindowBits; |
| } else if (this._options.clientMaxWindowBits == null) { |
| params.client_max_window_bits = true; |
| } |
|
|
| return params; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| accept(configurations) { |
| configurations = this.normalizeParams(configurations); |
|
|
| this.params = this._isServer |
| ? this.acceptAsServer(configurations) |
| : this.acceptAsClient(configurations); |
|
|
| return this.params; |
| } |
|
|
| |
| |
| |
| |
| |
| cleanup() { |
| if (this._inflate) { |
| this._inflate.close(); |
| this._inflate = null; |
| } |
|
|
| if (this._deflate) { |
| const callback = this._deflate[kCallback]; |
|
|
| this._deflate.close(); |
| this._deflate = null; |
|
|
| if (callback) { |
| callback( |
| new Error( |
| 'The deflate stream was closed while data was being processed' |
| ) |
| ); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| acceptAsServer(offers) { |
| const opts = this._options; |
| const accepted = offers.find((params) => { |
| if ( |
| (opts.serverNoContextTakeover === false && |
| params.server_no_context_takeover) || |
| (params.server_max_window_bits && |
| (opts.serverMaxWindowBits === false || |
| (typeof opts.serverMaxWindowBits === 'number' && |
| opts.serverMaxWindowBits > params.server_max_window_bits))) || |
| (typeof opts.clientMaxWindowBits === 'number' && |
| !params.client_max_window_bits) |
| ) { |
| return false; |
| } |
|
|
| return true; |
| }); |
|
|
| if (!accepted) { |
| throw new Error('None of the extension offers can be accepted'); |
| } |
|
|
| if (opts.serverNoContextTakeover) { |
| accepted.server_no_context_takeover = true; |
| } |
| if (opts.clientNoContextTakeover) { |
| accepted.client_no_context_takeover = true; |
| } |
| if (typeof opts.serverMaxWindowBits === 'number') { |
| accepted.server_max_window_bits = opts.serverMaxWindowBits; |
| } |
| if (typeof opts.clientMaxWindowBits === 'number') { |
| accepted.client_max_window_bits = opts.clientMaxWindowBits; |
| } else if ( |
| accepted.client_max_window_bits === true || |
| opts.clientMaxWindowBits === false |
| ) { |
| delete accepted.client_max_window_bits; |
| } |
|
|
| return accepted; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| acceptAsClient(response) { |
| const params = response[0]; |
|
|
| if ( |
| this._options.clientNoContextTakeover === false && |
| params.client_no_context_takeover |
| ) { |
| throw new Error('Unexpected parameter "client_no_context_takeover"'); |
| } |
|
|
| if (!params.client_max_window_bits) { |
| if (typeof this._options.clientMaxWindowBits === 'number') { |
| params.client_max_window_bits = this._options.clientMaxWindowBits; |
| } |
| } else if ( |
| this._options.clientMaxWindowBits === false || |
| (typeof this._options.clientMaxWindowBits === 'number' && |
| params.client_max_window_bits > this._options.clientMaxWindowBits) |
| ) { |
| throw new Error( |
| 'Unexpected or invalid parameter "client_max_window_bits"' |
| ); |
| } |
|
|
| return params; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| normalizeParams(configurations) { |
| configurations.forEach((params) => { |
| Object.keys(params).forEach((key) => { |
| let value = params[key]; |
|
|
| if (value.length > 1) { |
| throw new Error(`Parameter "${key}" must have only a single value`); |
| } |
|
|
| value = value[0]; |
|
|
| if (key === 'client_max_window_bits') { |
| if (value !== true) { |
| const num = +value; |
| if (!Number.isInteger(num) || num < 8 || num > 15) { |
| throw new TypeError( |
| `Invalid value for parameter "${key}": ${value}` |
| ); |
| } |
| value = num; |
| } else if (!this._isServer) { |
| throw new TypeError( |
| `Invalid value for parameter "${key}": ${value}` |
| ); |
| } |
| } else if (key === 'server_max_window_bits') { |
| const num = +value; |
| if (!Number.isInteger(num) || num < 8 || num > 15) { |
| throw new TypeError( |
| `Invalid value for parameter "${key}": ${value}` |
| ); |
| } |
| value = num; |
| } else if ( |
| key === 'client_no_context_takeover' || |
| key === 'server_no_context_takeover' |
| ) { |
| if (value !== true) { |
| throw new TypeError( |
| `Invalid value for parameter "${key}": ${value}` |
| ); |
| } |
| } else { |
| throw new Error(`Unknown parameter "${key}"`); |
| } |
|
|
| params[key] = value; |
| }); |
| }); |
|
|
| return configurations; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| decompress(data, fin, callback) { |
| zlibLimiter.add((done) => { |
| this._decompress(data, fin, (err, result) => { |
| done(); |
| callback(err, result); |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| compress(data, fin, callback) { |
| zlibLimiter.add((done) => { |
| this._compress(data, fin, (err, result) => { |
| done(); |
| callback(err, result); |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| _decompress(data, fin, callback) { |
| const endpoint = this._isServer ? 'client' : 'server'; |
|
|
| if (!this._inflate) { |
| const key = `${endpoint}_max_window_bits`; |
| const windowBits = |
| typeof this.params[key] !== 'number' |
| ? zlib.Z_DEFAULT_WINDOWBITS |
| : this.params[key]; |
|
|
| this._inflate = zlib.createInflateRaw({ |
| ...this._options.zlibInflateOptions, |
| windowBits |
| }); |
| this._inflate[kPerMessageDeflate] = this; |
| this._inflate[kTotalLength] = 0; |
| this._inflate[kBuffers] = []; |
| this._inflate.on('error', inflateOnError); |
| this._inflate.on('data', inflateOnData); |
| } |
|
|
| this._inflate[kCallback] = callback; |
|
|
| this._inflate.write(data); |
| if (fin) this._inflate.write(TRAILER); |
|
|
| this._inflate.flush(() => { |
| const err = this._inflate[kError]; |
|
|
| if (err) { |
| this._inflate.close(); |
| this._inflate = null; |
| callback(err); |
| return; |
| } |
|
|
| const data = bufferUtil.concat( |
| this._inflate[kBuffers], |
| this._inflate[kTotalLength] |
| ); |
|
|
| if (this._inflate._readableState.endEmitted) { |
| this._inflate.close(); |
| this._inflate = null; |
| } else { |
| this._inflate[kTotalLength] = 0; |
| this._inflate[kBuffers] = []; |
|
|
| if (fin && this.params[`${endpoint}_no_context_takeover`]) { |
| this._inflate.reset(); |
| } |
| } |
|
|
| callback(null, data); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| _compress(data, fin, callback) { |
| const endpoint = this._isServer ? 'server' : 'client'; |
|
|
| if (!this._deflate) { |
| const key = `${endpoint}_max_window_bits`; |
| const windowBits = |
| typeof this.params[key] !== 'number' |
| ? zlib.Z_DEFAULT_WINDOWBITS |
| : this.params[key]; |
|
|
| this._deflate = zlib.createDeflateRaw({ |
| ...this._options.zlibDeflateOptions, |
| windowBits |
| }); |
|
|
| this._deflate[kTotalLength] = 0; |
| this._deflate[kBuffers] = []; |
|
|
| this._deflate.on('data', deflateOnData); |
| } |
|
|
| this._deflate[kCallback] = callback; |
|
|
| this._deflate.write(data); |
| this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { |
| if (!this._deflate) { |
| |
| |
| |
| return; |
| } |
|
|
| let data = bufferUtil.concat( |
| this._deflate[kBuffers], |
| this._deflate[kTotalLength] |
| ); |
|
|
| if (fin) { |
| data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); |
| } |
|
|
| |
| |
| |
| |
| this._deflate[kCallback] = null; |
|
|
| this._deflate[kTotalLength] = 0; |
| this._deflate[kBuffers] = []; |
|
|
| if (fin && this.params[`${endpoint}_no_context_takeover`]) { |
| this._deflate.reset(); |
| } |
|
|
| callback(null, data); |
| }); |
| } |
| } |
|
|
| module.exports = PerMessageDeflate; |
|
|
| |
| |
| |
| |
| |
| |
| function deflateOnData(chunk) { |
| this[kBuffers].push(chunk); |
| this[kTotalLength] += chunk.length; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function inflateOnData(chunk) { |
| this[kTotalLength] += chunk.length; |
|
|
| if ( |
| this[kPerMessageDeflate]._maxPayload < 1 || |
| this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload |
| ) { |
| this[kBuffers].push(chunk); |
| return; |
| } |
|
|
| this[kError] = new RangeError('Max payload size exceeded'); |
| this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; |
| this[kError][kStatusCode] = 1009; |
| this.removeListener('data', inflateOnData); |
|
|
| |
| |
| |
| |
| |
| |
| |
| this.reset(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function inflateOnError(err) { |
| |
| |
| |
| |
| this[kPerMessageDeflate]._inflate = null; |
|
|
| if (this[kError]) { |
| this[kCallback](this[kError]); |
| return; |
| } |
|
|
| err[kStatusCode] = 1007; |
| this[kCallback](err); |
| } |
|
|