httsper 0.0.1-security → 7.5.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of httsper might be problematic. Click here for more details.

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