mkjs/server/node_modules/ws/lib/PerMessageDeflate.js

385 lines
10 KiB
JavaScript
Raw Normal View History

2017-09-08 09:24:16 -07:00
'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;