You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

502 lines
14 KiB

4 years ago
  1. 'use strict';
  2. const Limiter = require('async-limiter');
  3. const zlib = require('zlib');
  4. const bufferUtil = require('./buffer-util');
  5. const { kStatusCode, NOOP } = require('./constants');
  6. const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
  7. const EMPTY_BLOCK = Buffer.from([0x00]);
  8. const kPerMessageDeflate = Symbol('permessage-deflate');
  9. const kTotalLength = Symbol('total-length');
  10. const kCallback = Symbol('callback');
  11. const kBuffers = Symbol('buffers');
  12. const kError = Symbol('error');
  13. //
  14. // We limit zlib concurrency, which prevents severe memory fragmentation
  15. // as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
  16. // and https://github.com/websockets/ws/issues/1202
  17. //
  18. // Intentionally global; it's the global thread pool that's an issue.
  19. //
  20. let zlibLimiter;
  21. /**
  22. * permessage-deflate implementation.
  23. */
  24. class PerMessageDeflate {
  25. /**
  26. * Creates a PerMessageDeflate instance.
  27. *
  28. * @param {Object} options Configuration options
  29. * @param {Boolean} options.serverNoContextTakeover Request/accept disabling
  30. * of server context takeover
  31. * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge
  32. * disabling of client context takeover
  33. * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the
  34. * use of a custom server window size
  35. * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support
  36. * for, or request, a custom client window size
  37. * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate
  38. * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate
  39. * @param {Number} options.threshold Size (in bytes) below which messages
  40. * should not be compressed
  41. * @param {Number} options.concurrencyLimit The number of concurrent calls to
  42. * zlib
  43. * @param {Boolean} isServer Create the instance in either server or client
  44. * mode
  45. * @param {Number} maxPayload The maximum allowed message length
  46. */
  47. constructor(options, isServer, maxPayload) {
  48. this._maxPayload = maxPayload | 0;
  49. this._options = options || {};
  50. this._threshold =
  51. this._options.threshold !== undefined ? this._options.threshold : 1024;
  52. this._isServer = !!isServer;
  53. this._deflate = null;
  54. this._inflate = null;
  55. this.params = null;
  56. if (!zlibLimiter) {
  57. const concurrency =
  58. this._options.concurrencyLimit !== undefined
  59. ? this._options.concurrencyLimit
  60. : 10;
  61. zlibLimiter = new Limiter({ concurrency });
  62. }
  63. }
  64. /**
  65. * @type {String}
  66. */
  67. static get extensionName() {
  68. return 'permessage-deflate';
  69. }
  70. /**
  71. * Create an extension negotiation offer.
  72. *
  73. * @return {Object} Extension parameters
  74. * @public
  75. */
  76. offer() {
  77. const params = {};
  78. if (this._options.serverNoContextTakeover) {
  79. params.server_no_context_takeover = true;
  80. }
  81. if (this._options.clientNoContextTakeover) {
  82. params.client_no_context_takeover = true;
  83. }
  84. if (this._options.serverMaxWindowBits) {
  85. params.server_max_window_bits = this._options.serverMaxWindowBits;
  86. }
  87. if (this._options.clientMaxWindowBits) {
  88. params.client_max_window_bits = this._options.clientMaxWindowBits;
  89. } else if (this._options.clientMaxWindowBits == null) {
  90. params.client_max_window_bits = true;
  91. }
  92. return params;
  93. }
  94. /**
  95. * Accept an extension negotiation offer/response.
  96. *
  97. * @param {Array} configurations The extension negotiation offers/reponse
  98. * @return {Object} Accepted configuration
  99. * @public
  100. */
  101. accept(configurations) {
  102. configurations = this.normalizeParams(configurations);
  103. this.params = this._isServer
  104. ? this.acceptAsServer(configurations)
  105. : this.acceptAsClient(configurations);
  106. return this.params;
  107. }
  108. /**
  109. * Releases all resources used by the extension.
  110. *
  111. * @public
  112. */
  113. cleanup() {
  114. if (this._inflate) {
  115. this._inflate.close();
  116. this._inflate = null;
  117. }
  118. if (this._deflate) {
  119. this._deflate.close();
  120. this._deflate = null;
  121. }
  122. }
  123. /**
  124. * Accept an extension negotiation offer.
  125. *
  126. * @param {Array} offers The extension negotiation offers
  127. * @return {Object} Accepted configuration
  128. * @private
  129. */
  130. acceptAsServer(offers) {
  131. const opts = this._options;
  132. const accepted = offers.find((params) => {
  133. if (
  134. (opts.serverNoContextTakeover === false &&
  135. params.server_no_context_takeover) ||
  136. (params.server_max_window_bits &&
  137. (opts.serverMaxWindowBits === false ||
  138. (typeof opts.serverMaxWindowBits === 'number' &&
  139. opts.serverMaxWindowBits > params.server_max_window_bits))) ||
  140. (typeof opts.clientMaxWindowBits === 'number' &&
  141. !params.client_max_window_bits)
  142. ) {
  143. return false;
  144. }
  145. return true;
  146. });
  147. if (!accepted) {
  148. throw new Error('None of the extension offers can be accepted');
  149. }
  150. if (opts.serverNoContextTakeover) {
  151. accepted.server_no_context_takeover = true;
  152. }
  153. if (opts.clientNoContextTakeover) {
  154. accepted.client_no_context_takeover = true;
  155. }
  156. if (typeof opts.serverMaxWindowBits === 'number') {
  157. accepted.server_max_window_bits = opts.serverMaxWindowBits;
  158. }
  159. if (typeof opts.clientMaxWindowBits === 'number') {
  160. accepted.client_max_window_bits = opts.clientMaxWindowBits;
  161. } else if (
  162. accepted.client_max_window_bits === true ||
  163. opts.clientMaxWindowBits === false
  164. ) {
  165. delete accepted.client_max_window_bits;
  166. }
  167. return accepted;
  168. }
  169. /**
  170. * Accept the extension negotiation response.
  171. *
  172. * @param {Array} response The extension negotiation response
  173. * @return {Object} Accepted configuration
  174. * @private
  175. */
  176. acceptAsClient(response) {
  177. const params = response[0];
  178. if (
  179. this._options.clientNoContextTakeover === false &&
  180. params.client_no_context_takeover
  181. ) {
  182. throw new Error('Unexpected parameter "client_no_context_takeover"');
  183. }
  184. if (!params.client_max_window_bits) {
  185. if (typeof this._options.clientMaxWindowBits === 'number') {
  186. params.client_max_window_bits = this._options.clientMaxWindowBits;
  187. }
  188. } else if (
  189. this._options.clientMaxWindowBits === false ||
  190. (typeof this._options.clientMaxWindowBits === 'number' &&
  191. params.client_max_window_bits > this._options.clientMaxWindowBits)
  192. ) {
  193. throw new Error(
  194. 'Unexpected or invalid parameter "client_max_window_bits"'
  195. );
  196. }
  197. return params;
  198. }
  199. /**
  200. * Normalize parameters.
  201. *
  202. * @param {Array} configurations The extension negotiation offers/reponse
  203. * @return {Array} The offers/response with normalized parameters
  204. * @private
  205. */
  206. normalizeParams(configurations) {
  207. configurations.forEach((params) => {
  208. Object.keys(params).forEach((key) => {
  209. var value = params[key];
  210. if (value.length > 1) {
  211. throw new Error(`Parameter "${key}" must have only a single value`);
  212. }
  213. value = value[0];
  214. if (key === 'client_max_window_bits') {
  215. if (value !== true) {
  216. const num = +value;
  217. if (!Number.isInteger(num) || num < 8 || num > 15) {
  218. throw new TypeError(
  219. `Invalid value for parameter "${key}": ${value}`
  220. );
  221. }
  222. value = num;
  223. } else if (!this._isServer) {
  224. throw new TypeError(
  225. `Invalid value for parameter "${key}": ${value}`
  226. );
  227. }
  228. } else if (key === 'server_max_window_bits') {
  229. const num = +value;
  230. if (!Number.isInteger(num) || num < 8 || num > 15) {
  231. throw new TypeError(
  232. `Invalid value for parameter "${key}": ${value}`
  233. );
  234. }
  235. value = num;
  236. } else if (
  237. key === 'client_no_context_takeover' ||
  238. key === 'server_no_context_takeover'
  239. ) {
  240. if (value !== true) {
  241. throw new TypeError(
  242. `Invalid value for parameter "${key}": ${value}`
  243. );
  244. }
  245. } else {
  246. throw new Error(`Unknown parameter "${key}"`);
  247. }
  248. params[key] = value;
  249. });
  250. });
  251. return configurations;
  252. }
  253. /**
  254. * Decompress data. Concurrency limited by async-limiter.
  255. *
  256. * @param {Buffer} data Compressed data
  257. * @param {Boolean} fin Specifies whether or not this is the last fragment
  258. * @param {Function} callback Callback
  259. * @public
  260. */
  261. decompress(data, fin, callback) {
  262. zlibLimiter.push((done) => {
  263. this._decompress(data, fin, (err, result) => {
  264. done();
  265. callback(err, result);
  266. });
  267. });
  268. }
  269. /**
  270. * Compress data. Concurrency limited by async-limiter.
  271. *
  272. * @param {Buffer} data Data to compress
  273. * @param {Boolean} fin Specifies whether or not this is the last fragment
  274. * @param {Function} callback Callback
  275. * @public
  276. */
  277. compress(data, fin, callback) {
  278. zlibLimiter.push((done) => {
  279. this._compress(data, fin, (err, result) => {
  280. done();
  281. callback(err, result);
  282. });
  283. });
  284. }
  285. /**
  286. * Decompress data.
  287. *
  288. * @param {Buffer} data Compressed data
  289. * @param {Boolean} fin Specifies whether or not this is the last fragment
  290. * @param {Function} callback Callback
  291. * @private
  292. */
  293. _decompress(data, fin, callback) {
  294. const endpoint = this._isServer ? 'client' : 'server';
  295. if (!this._inflate) {
  296. const key = `${endpoint}_max_window_bits`;
  297. const windowBits =
  298. typeof this.params[key] !== 'number'
  299. ? zlib.Z_DEFAULT_WINDOWBITS
  300. : this.params[key];
  301. this._inflate = zlib.createInflateRaw(
  302. Object.assign({}, this._options.zlibInflateOptions, { windowBits })
  303. );
  304. this._inflate[kPerMessageDeflate] = this;
  305. this._inflate[kTotalLength] = 0;
  306. this._inflate[kBuffers] = [];
  307. this._inflate.on('error', inflateOnError);
  308. this._inflate.on('data', inflateOnData);
  309. }
  310. this._inflate[kCallback] = callback;
  311. this._inflate.write(data);
  312. if (fin) this._inflate.write(TRAILER);
  313. this._inflate.flush(() => {
  314. const err = this._inflate[kError];
  315. if (err) {
  316. this._inflate.close();
  317. this._inflate = null;
  318. callback(err);
  319. return;
  320. }
  321. const data = bufferUtil.concat(
  322. this._inflate[kBuffers],
  323. this._inflate[kTotalLength]
  324. );
  325. if (fin && this.params[`${endpoint}_no_context_takeover`]) {
  326. this._inflate.close();
  327. this._inflate = null;
  328. } else {
  329. this._inflate[kTotalLength] = 0;
  330. this._inflate[kBuffers] = [];
  331. }
  332. callback(null, data);
  333. });
  334. }
  335. /**
  336. * Compress data.
  337. *
  338. * @param {Buffer} data Data to compress
  339. * @param {Boolean} fin Specifies whether or not this is the last fragment
  340. * @param {Function} callback Callback
  341. * @private
  342. */
  343. _compress(data, fin, callback) {
  344. if (!data || data.length === 0) {
  345. process.nextTick(callback, null, EMPTY_BLOCK);
  346. return;
  347. }
  348. const endpoint = this._isServer ? 'server' : 'client';
  349. if (!this._deflate) {
  350. const key = `${endpoint}_max_window_bits`;
  351. const windowBits =
  352. typeof this.params[key] !== 'number'
  353. ? zlib.Z_DEFAULT_WINDOWBITS
  354. : this.params[key];
  355. this._deflate = zlib.createDeflateRaw(
  356. Object.assign({}, this._options.zlibDeflateOptions, { windowBits })
  357. );
  358. this._deflate[kTotalLength] = 0;
  359. this._deflate[kBuffers] = [];
  360. //
  361. // An `'error'` event is emitted, only on Node.js < 10.0.0, if the
  362. // `zlib.DeflateRaw` instance is closed while data is being processed.
  363. // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong
  364. // time due to an abnormal WebSocket closure.
  365. //
  366. this._deflate.on('error', NOOP);
  367. this._deflate.on('data', deflateOnData);
  368. }
  369. this._deflate.write(data);
  370. this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
  371. if (!this._deflate) {
  372. //
  373. // This `if` statement is only needed for Node.js < 10.0.0 because as of
  374. // commit https://github.com/nodejs/node/commit/5e3f5164, the flush
  375. // callback is no longer called if the deflate stream is closed while
  376. // data is being processed.
  377. //
  378. return;
  379. }
  380. var data = bufferUtil.concat(
  381. this._deflate[kBuffers],
  382. this._deflate[kTotalLength]
  383. );
  384. if (fin) data = data.slice(0, data.length - 4);
  385. if (fin && this.params[`${endpoint}_no_context_takeover`]) {
  386. this._deflate.close();
  387. this._deflate = null;
  388. } else {
  389. this._deflate[kTotalLength] = 0;
  390. this._deflate[kBuffers] = [];
  391. }
  392. callback(null, data);
  393. });
  394. }
  395. }
  396. module.exports = PerMessageDeflate;
  397. /**
  398. * The listener of the `zlib.DeflateRaw` stream `'data'` event.
  399. *
  400. * @param {Buffer} chunk A chunk of data
  401. * @private
  402. */
  403. function deflateOnData(chunk) {
  404. this[kBuffers].push(chunk);
  405. this[kTotalLength] += chunk.length;
  406. }
  407. /**
  408. * The listener of the `zlib.InflateRaw` stream `'data'` event.
  409. *
  410. * @param {Buffer} chunk A chunk of data
  411. * @private
  412. */
  413. function inflateOnData(chunk) {
  414. this[kTotalLength] += chunk.length;
  415. if (
  416. this[kPerMessageDeflate]._maxPayload < 1 ||
  417. this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload
  418. ) {
  419. this[kBuffers].push(chunk);
  420. return;
  421. }
  422. this[kError] = new RangeError('Max payload size exceeded');
  423. this[kError][kStatusCode] = 1009;
  424. this.removeListener('data', inflateOnData);
  425. this.reset();
  426. }
  427. /**
  428. * The listener of the `zlib.InflateRaw` stream `'error'` event.
  429. *
  430. * @param {Error} err The emitted error
  431. * @private
  432. */
  433. function inflateOnError(err) {
  434. //
  435. // There is no need to call `Zlib#close()` as the handle is automatically
  436. // closed when an error is emitted.
  437. //
  438. this[kPerMessageDeflate]._inflate = null;
  439. err[kStatusCode] = 1007;
  440. this[kCallback](err);
  441. }