'use strict'; const safeBuffer = require('safe-buffer'); const zlib = require('zlib'); const bufferUtil = require('./BufferUtil'); const Buffer = safeBuffer.Buffer; const AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15]; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const EMPTY_BLOCK = Buffer.from([0x00]); const DEFAULT_WINDOW_BITS = 15; const DEFAULT_MEM_LEVEL = 8; /** * Per-message Deflate implementation. */ class PerMessageDeflate { constructor (options, isServer, maxPayload) { this._options = options || {}; this._isServer = !!isServer; this._inflate = null; this._deflate = null; this.params = null; this._maxPayload = maxPayload || 0; this.threshold = this._options.threshold === undefined ? 1024 : this._options.threshold; } static get extensionName () { return 'permessage-deflate'; } /** * Create extension parameters offer. * * @return {Object} Extension parameters * @public */ 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 extension offer. * * @param {Array} paramsList Extension parameters * @return {Object} Accepted configuration * @public */ accept (paramsList) { paramsList = this.normalizeParams(paramsList); var params; if (this._isServer) { params = this.acceptAsServer(paramsList); } else { params = this.acceptAsClient(paramsList); } this.params = params; return params; } /** * Releases all resources used by the extension. * * @public */ cleanup () { if (this._inflate) { if (this._inflate.writeInProgress) { this._inflate.pendingClose = true; } else { this._inflate.close(); this._inflate = null; } } if (this._deflate) { if (this._deflate.writeInProgress) { this._deflate.pendingClose = true; } else { this._deflate.close(); this._deflate = null; } } } /** * Accept extension offer from client. * * @param {Array} paramsList Extension parameters * @return {Object} Accepted configuration * @private */ acceptAsServer (paramsList) { const accepted = {}; const result = paramsList.some((params) => { if (( this._options.serverNoContextTakeover === false && params.server_no_context_takeover ) || ( this._options.serverMaxWindowBits === false && params.server_max_window_bits ) || ( typeof this._options.serverMaxWindowBits === 'number' && typeof params.server_max_window_bits === 'number' && this._options.serverMaxWindowBits > params.server_max_window_bits ) || ( typeof this._options.clientMaxWindowBits === 'number' && !params.client_max_window_bits )) { return; } if ( this._options.serverNoContextTakeover || params.server_no_context_takeover ) { accepted.server_no_context_takeover = true; } if (this._options.clientNoContextTakeover) { accepted.client_no_context_takeover = true; } if ( this._options.clientNoContextTakeover !== false && params.client_no_context_takeover ) { accepted.client_no_context_takeover = true; } if (typeof this._options.serverMaxWindowBits === 'number') { accepted.server_max_window_bits = this._options.serverMaxWindowBits; } else if (typeof params.server_max_window_bits === 'number') { accepted.server_max_window_bits = params.server_max_window_bits; } if (typeof this._options.clientMaxWindowBits === 'number') { accepted.client_max_window_bits = this._options.clientMaxWindowBits; } else if ( this._options.clientMaxWindowBits !== false && typeof params.client_max_window_bits === 'number' ) { accepted.client_max_window_bits = params.client_max_window_bits; } return true; }); if (!result) throw new Error(`Doesn't support the offered configuration`); return accepted; } /** * Accept extension response from server. * * @param {Array} paramsList Extension parameters * @return {Object} Accepted configuration * @private */ acceptAsClient (paramsList) { const params = paramsList[0]; 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.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' && ( !params.client_max_window_bits || params.client_max_window_bits > this._options.clientMaxWindowBits )) { throw new Error('Invalid value for "client_max_window_bits"'); } } return params; } /** * Normalize extensions parameters. * * @param {Array} paramsList Extension parameters * @return {Array} Normalized extensions parameters * @private */ normalizeParams (paramsList) { return paramsList.map((params) => { Object.keys(params).forEach((key) => { var value = params[key]; if (value.length > 1) { throw new Error(`Multiple extension parameters for ${key}`); } value = value[0]; switch (key) { case 'server_no_context_takeover': case 'client_no_context_takeover': if (value !== true) { throw new Error(`invalid extension parameter value for ${key} (${value})`); } params[key] = true; break; case 'server_max_window_bits': case 'client_max_window_bits': if (typeof value === 'string') { value = parseInt(value, 10); if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) { throw new Error(`invalid extension parameter value for ${key} (${value})`); } } if (!this._isServer && value === true) { throw new Error(`Missing extension parameter value for ${key}`); } params[key] = value; break; default: throw new Error(`Not defined extension parameter (${key})`); } }); return params; }); } /** * Decompress data. * * @param {Buffer} data Compressed data * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public */ decompress (data, fin, callback) { const endpoint = this._isServer ? 'client' : 'server'; if (!this._inflate) { const maxWindowBits = this.params[`${endpoint}_max_window_bits`]; this._inflate = zlib.createInflateRaw({ windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS }); } this._inflate.writeInProgress = true; var totalLength = 0; const buffers = []; var err; const onData = (data) => { totalLength += data.length; if (this._maxPayload < 1 || totalLength <= this._maxPayload) { return buffers.push(data); } err = new Error('max payload size exceeded'); err.closeCode = 1009; this._inflate.reset(); }; const onError = (err) => { cleanup(); callback(err); }; const cleanup = () => { if (!this._inflate) return; this._inflate.removeListener('error', onError); this._inflate.removeListener('data', onData); this._inflate.writeInProgress = false; if ( (fin && this.params[`${endpoint}_no_context_takeover`]) || this._inflate.pendingClose ) { this._inflate.close(); this._inflate = null; } }; this._inflate.on('error', onError).on('data', onData); this._inflate.write(data); if (fin) this._inflate.write(TRAILER); this._inflate.flush(() => { cleanup(); if (err) callback(err); else callback(null, bufferUtil.concat(buffers, totalLength)); }); } /** * Compress data. * * @param {Buffer} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public */ compress (data, fin, callback) { if (!data || data.length === 0) { process.nextTick(callback, null, EMPTY_BLOCK); return; } const endpoint = this._isServer ? 'server' : 'client'; if (!this._deflate) { const maxWindowBits = this.params[`${endpoint}_max_window_bits`]; this._deflate = zlib.createDeflateRaw({ flush: zlib.Z_SYNC_FLUSH, windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS, memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL }); } this._deflate.writeInProgress = true; var totalLength = 0; const buffers = []; const onData = (data) => { totalLength += data.length; buffers.push(data); }; const onError = (err) => { cleanup(); callback(err); }; const cleanup = () => { if (!this._deflate) return; this._deflate.removeListener('error', onError); this._deflate.removeListener('data', onData); this._deflate.writeInProgress = false; if ( (fin && this.params[`${endpoint}_no_context_takeover`]) || this._deflate.pendingClose ) { this._deflate.close(); this._deflate = null; } }; this._deflate.on('error', onError).on('data', onData); this._deflate.write(data); this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { cleanup(); var data = bufferUtil.concat(buffers, totalLength); if (fin) data = data.slice(0, data.length - 4); callback(null, data); }); } } module.exports = PerMessageDeflate;