'use strict'; const zlib = require('zlib'); const AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15]; const DEFAULT_WINDOW_BITS = 15; const DEFAULT_MEM_LEVEL = 8; const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); const EMPTY_BLOCK = Buffer.from([0x00]); /** * Per-message Compression Extensions 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; } /** * Create extension parameters offer * * @api public */ offer () { var 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 * * @api 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 * * @api 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 * * @api private */ acceptAsServer (paramsList) { var accepted = {}; var result = paramsList.some((params) => { accepted = {}; if (this._options.serverNoContextTakeover === false && params.server_no_context_takeover) { return; } if (this._options.serverMaxWindowBits === false && params.server_max_window_bits) { return; } if (typeof this._options.serverMaxWindowBits === 'number' && typeof params.server_max_window_bits === 'number' && this._options.serverMaxWindowBits > params.server_max_window_bits) { return; } if (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 * * @api privaye */ acceptAsClient (paramsList) { var 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 * * @api 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, Buffer.concat(buffers, totalLength)); }); } /** * Compress message * * @api public */ compress (data, fin, callback) { if (!data || data.length === 0) { return callback(null, EMPTY_BLOCK); } var endpoint = this._isServer ? 'server' : 'client'; if (!this._deflate) { var 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; const buffers = []; const onData = (data) => 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 = Buffer.concat(buffers); if (fin) { data = data.slice(0, data.length - 4); } callback(null, data); }); } } PerMessageDeflate.extensionName = 'permessage-deflate'; module.exports = PerMessageDeflate;