fastify 3.29.3 → 3.29.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fastify.js +1 -1
- package/lib/contentTypeParser.js +70 -6
- package/package.json +3 -2
- package/test/build-certificate.js +6 -7
- package/test/content-parser.test.js +257 -0
- package/test/custom-parser.test.js +3 -3
package/fastify.js
CHANGED
package/lib/contentTypeParser.js
CHANGED
|
@@ -6,6 +6,7 @@ let lru = require('tiny-lru')
|
|
|
6
6
|
// See https://github.com/fastify/fastify/issues/2356
|
|
7
7
|
// and https://github.com/fastify/fastify/discussions/2907.
|
|
8
8
|
lru = typeof lru === 'function' ? lru : lru.default
|
|
9
|
+
const { safeParse: safeParseContentType, defaultContentType } = require('fast-content-type-parse')
|
|
9
10
|
|
|
10
11
|
const secureJson = require('secure-json-parse')
|
|
11
12
|
const {
|
|
@@ -36,7 +37,7 @@ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning)
|
|
|
36
37
|
this.customParsers = new Map()
|
|
37
38
|
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
|
|
38
39
|
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
|
|
39
|
-
this.parserList = ['application/json', 'text/plain']
|
|
40
|
+
this.parserList = [new ParserListItem('application/json'), new ParserListItem('text/plain')]
|
|
40
41
|
this.parserRegExpList = []
|
|
41
42
|
this.cache = lru(100)
|
|
42
43
|
}
|
|
@@ -69,8 +70,9 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
69
70
|
this.customParsers.set('', parser)
|
|
70
71
|
} else {
|
|
71
72
|
if (contentTypeIsString) {
|
|
72
|
-
this.parserList.unshift(contentType)
|
|
73
|
+
this.parserList.unshift(new ParserListItem(contentType))
|
|
73
74
|
} else {
|
|
75
|
+
contentType.isEssence = contentType.source.indexOf(';') === -1
|
|
74
76
|
this.parserRegExpList.unshift(contentType)
|
|
75
77
|
}
|
|
76
78
|
this.customParsers.set(contentType.toString(), parser)
|
|
@@ -97,11 +99,25 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
97
99
|
return this.customParsers.get(contentType)
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
const parser = this.cache.get(contentType)
|
|
103
|
+
// TODO not covered by tests, this is a security backport
|
|
104
|
+
/* istanbul ignore next */
|
|
105
|
+
if (parser !== undefined) return parser
|
|
106
|
+
|
|
107
|
+
const parsed = safeParseContentType(contentType)
|
|
108
|
+
|
|
109
|
+
// dummyContentType always the same object
|
|
110
|
+
// we can use === for the comparsion and return early
|
|
111
|
+
if (parsed === defaultContentType) {
|
|
112
|
+
return this.customParsers.get('')
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
// eslint-disable-next-line no-var
|
|
101
116
|
for (var i = 0; i !== this.parserList.length; ++i) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
const parser = this.customParsers.get(
|
|
117
|
+
const parserListItem = this.parserList[i]
|
|
118
|
+
if (compareContentType(parsed, parserListItem)) {
|
|
119
|
+
const parser = this.customParsers.get(parserListItem.name)
|
|
120
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
105
121
|
this.cache.set(contentType, parser)
|
|
106
122
|
return parser
|
|
107
123
|
}
|
|
@@ -110,8 +126,9 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
110
126
|
// eslint-disable-next-line no-var
|
|
111
127
|
for (var j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
112
128
|
const parserRegExp = this.parserRegExpList[j]
|
|
113
|
-
if (
|
|
129
|
+
if (compareRegExpContentType(contentType, parsed.type, parserRegExp)) {
|
|
114
130
|
const parser = this.customParsers.get(parserRegExp.toString())
|
|
131
|
+
// we set request content-type in cache to reduce parsing of MIME type
|
|
115
132
|
this.cache.set(contentType, parser)
|
|
116
133
|
return parser
|
|
117
134
|
}
|
|
@@ -297,6 +314,7 @@ function buildContentTypeParser (c) {
|
|
|
297
314
|
contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
|
|
298
315
|
contentTypeParser.customParsers = new Map(c.customParsers.entries())
|
|
299
316
|
contentTypeParser.parserList = c.parserList.slice()
|
|
317
|
+
contentTypeParser.parserRegExpList = c.parserRegExpList.slice()
|
|
300
318
|
return contentTypeParser
|
|
301
319
|
}
|
|
302
320
|
|
|
@@ -348,6 +366,52 @@ function removeAllContentTypeParsers () {
|
|
|
348
366
|
this[kContentTypeParser].removeAll()
|
|
349
367
|
}
|
|
350
368
|
|
|
369
|
+
function compareContentType (contentType, parserListItem) {
|
|
370
|
+
if (parserListItem.isEssence) {
|
|
371
|
+
// we do essence check
|
|
372
|
+
return contentType.type.indexOf(parserListItem) !== -1
|
|
373
|
+
} else {
|
|
374
|
+
// when the content-type includes parameters
|
|
375
|
+
// we do a full-text search
|
|
376
|
+
// reject essence content-type before checking parameters
|
|
377
|
+
if (contentType.type.indexOf(parserListItem.type) === -1) return false
|
|
378
|
+
for (const key of parserListItem.parameterKeys) {
|
|
379
|
+
// reject when missing parameters
|
|
380
|
+
if (!(key in contentType.parameters)) return false
|
|
381
|
+
// reject when parameters do not match
|
|
382
|
+
if (contentType.parameters[key] !== parserListItem.parameters[key]) return false
|
|
383
|
+
}
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function compareRegExpContentType (contentType, essenceMIMEType, regexp) {
|
|
389
|
+
if (regexp.isEssence) {
|
|
390
|
+
// we do essence check
|
|
391
|
+
return regexp.test(essenceMIMEType)
|
|
392
|
+
} else {
|
|
393
|
+
// when the content-type includes parameters
|
|
394
|
+
// we do a full-text match
|
|
395
|
+
return regexp.test(contentType)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function ParserListItem (contentType) {
|
|
400
|
+
this.name = contentType
|
|
401
|
+
// we pre-calculate all the needed information
|
|
402
|
+
// before content-type comparsion
|
|
403
|
+
const parsed = safeParseContentType(contentType)
|
|
404
|
+
this.type = parsed.type
|
|
405
|
+
this.parameters = parsed.parameters
|
|
406
|
+
this.parameterKeys = Object.keys(parsed.parameters)
|
|
407
|
+
this.isEssence = contentType.indexOf(';') === -1
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// used in ContentTypeParser.remove
|
|
411
|
+
ParserListItem.prototype.toString = function () {
|
|
412
|
+
return this.name
|
|
413
|
+
}
|
|
414
|
+
|
|
351
415
|
module.exports = ContentTypeParser
|
|
352
416
|
module.exports.helpers = {
|
|
353
417
|
buildContentTypeParser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify",
|
|
3
|
-
"version": "3.29.
|
|
3
|
+
"version": "3.29.5",
|
|
4
4
|
"description": "Fast and low overhead web framework, for Node.js",
|
|
5
5
|
"main": "fastify.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -158,9 +158,9 @@
|
|
|
158
158
|
"ienoopen": "^1.1.0",
|
|
159
159
|
"JSONStream": "^1.3.5",
|
|
160
160
|
"license-checker": "^25.0.1",
|
|
161
|
-
"pem": "^1.14.4",
|
|
162
161
|
"proxyquire": "^2.1.3",
|
|
163
162
|
"pump": "^3.0.0",
|
|
163
|
+
"self-cert": "^2.0.0",
|
|
164
164
|
"send": "^0.17.1",
|
|
165
165
|
"serve-static": "^1.14.1",
|
|
166
166
|
"simple-get": "^4.0.0",
|
|
@@ -181,6 +181,7 @@
|
|
|
181
181
|
"@fastify/error": "^2.0.0",
|
|
182
182
|
"abstract-logging": "^2.0.0",
|
|
183
183
|
"avvio": "^7.1.2",
|
|
184
|
+
"fast-content-type-parse": "^1.0.0",
|
|
184
185
|
"fast-json-stringify": "^2.5.2",
|
|
185
186
|
"find-my-way": "^4.5.0",
|
|
186
187
|
"flatstr": "^1.0.12",
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const pem = require('pem')
|
|
5
|
-
|
|
6
|
-
const createCertificate = util.promisify(pem.createCertificate)
|
|
3
|
+
const selfCert = require('self-cert')
|
|
7
4
|
|
|
8
5
|
async function buildCertificate () {
|
|
9
6
|
// "global" is used in here because "t.context" is only supported by "t.beforeEach" and "t.afterEach"
|
|
10
7
|
// For the test case which execute this code which will be using `t.before` and it can reduce the
|
|
11
8
|
// number of times executing it.
|
|
12
9
|
if (!global.context || !global.context.cert || !global.context.key) {
|
|
13
|
-
const
|
|
10
|
+
const certs = selfCert({
|
|
11
|
+
expires: new Date(Date.now() + 86400000)
|
|
12
|
+
})
|
|
14
13
|
global.context = {
|
|
15
|
-
cert:
|
|
16
|
-
key:
|
|
14
|
+
cert: certs.certificate,
|
|
15
|
+
key: certs.privateKey
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
}
|
|
@@ -328,3 +328,260 @@ test('Safeguard against malicious content-type / 3', async t => {
|
|
|
328
328
|
|
|
329
329
|
t.same(response.statusCode, 415)
|
|
330
330
|
})
|
|
331
|
+
|
|
332
|
+
test('Safeguard against content-type spoofing - string', async t => {
|
|
333
|
+
t.plan(1)
|
|
334
|
+
|
|
335
|
+
const fastify = Fastify()
|
|
336
|
+
fastify.removeAllContentTypeParsers()
|
|
337
|
+
fastify.addContentTypeParser('text/plain', function (request, body, done) {
|
|
338
|
+
t.pass('should be called')
|
|
339
|
+
done(null, body)
|
|
340
|
+
})
|
|
341
|
+
fastify.addContentTypeParser('application/json', function (request, body, done) {
|
|
342
|
+
t.fail('shouldn\'t be called')
|
|
343
|
+
done(null, body)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
fastify.post('/', async () => {
|
|
347
|
+
return 'ok'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
await fastify.inject({
|
|
351
|
+
method: 'POST',
|
|
352
|
+
path: '/',
|
|
353
|
+
headers: {
|
|
354
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
355
|
+
},
|
|
356
|
+
body: ''
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('Safeguard against content-type spoofing - regexp', async t => {
|
|
361
|
+
t.plan(1)
|
|
362
|
+
|
|
363
|
+
const fastify = Fastify()
|
|
364
|
+
fastify.removeAllContentTypeParsers()
|
|
365
|
+
fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {
|
|
366
|
+
t.pass('should be called')
|
|
367
|
+
done(null, body)
|
|
368
|
+
})
|
|
369
|
+
fastify.addContentTypeParser(/application\/json/, function (request, body, done) {
|
|
370
|
+
t.fail('shouldn\'t be called')
|
|
371
|
+
done(null, body)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
fastify.post('/', async () => {
|
|
375
|
+
return 'ok'
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
await fastify.inject({
|
|
379
|
+
method: 'POST',
|
|
380
|
+
path: '/',
|
|
381
|
+
headers: {
|
|
382
|
+
'content-type': 'text/plain; content-type="application/json"'
|
|
383
|
+
},
|
|
384
|
+
body: ''
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('content-type match parameters - string 1', async t => {
|
|
389
|
+
t.plan(1)
|
|
390
|
+
|
|
391
|
+
const fastify = Fastify()
|
|
392
|
+
fastify.removeAllContentTypeParsers()
|
|
393
|
+
fastify.addContentTypeParser('text/plain; charset=utf8', function (request, body, done) {
|
|
394
|
+
t.fail('shouldn\'t be called')
|
|
395
|
+
done(null, body)
|
|
396
|
+
})
|
|
397
|
+
fastify.addContentTypeParser('application/json; charset=utf8', function (request, body, done) {
|
|
398
|
+
t.pass('should be called')
|
|
399
|
+
done(null, body)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
fastify.post('/', async () => {
|
|
403
|
+
return 'ok'
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await fastify.inject({
|
|
407
|
+
method: 'POST',
|
|
408
|
+
path: '/',
|
|
409
|
+
headers: {
|
|
410
|
+
'content-type': 'application/json; charset=utf8'
|
|
411
|
+
},
|
|
412
|
+
body: ''
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('content-type match parameters - string 2', async t => {
|
|
417
|
+
t.plan(1)
|
|
418
|
+
|
|
419
|
+
const fastify = Fastify()
|
|
420
|
+
fastify.removeAllContentTypeParsers()
|
|
421
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
422
|
+
t.pass('should be called')
|
|
423
|
+
done(null, body)
|
|
424
|
+
})
|
|
425
|
+
fastify.addContentTypeParser('text/plain; charset=utf8; foo=bar', function (request, body, done) {
|
|
426
|
+
t.fail('shouldn\'t be called')
|
|
427
|
+
done(null, body)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
fastify.post('/', async () => {
|
|
431
|
+
return 'ok'
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
await fastify.inject({
|
|
435
|
+
method: 'POST',
|
|
436
|
+
path: '/',
|
|
437
|
+
headers: {
|
|
438
|
+
'content-type': 'application/json; foo=bar; charset=utf8'
|
|
439
|
+
},
|
|
440
|
+
body: ''
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('content-type match parameters - regexp', async t => {
|
|
445
|
+
t.plan(1)
|
|
446
|
+
|
|
447
|
+
const fastify = Fastify()
|
|
448
|
+
fastify.removeAllContentTypeParsers()
|
|
449
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {
|
|
450
|
+
t.pass('should be called')
|
|
451
|
+
done(null, body)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
fastify.post('/', async () => {
|
|
455
|
+
return 'ok'
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
await fastify.inject({
|
|
459
|
+
method: 'POST',
|
|
460
|
+
path: '/',
|
|
461
|
+
headers: {
|
|
462
|
+
'content-type': 'application/json; charset=utf8'
|
|
463
|
+
},
|
|
464
|
+
body: ''
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('content-type fail when parameters not match - string 1', async t => {
|
|
469
|
+
t.plan(1)
|
|
470
|
+
|
|
471
|
+
const fastify = Fastify()
|
|
472
|
+
fastify.removeAllContentTypeParsers()
|
|
473
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
474
|
+
t.fail('shouldn\'t be called')
|
|
475
|
+
done(null, body)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
fastify.post('/', async () => {
|
|
479
|
+
return 'ok'
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
const response = await fastify.inject({
|
|
483
|
+
method: 'POST',
|
|
484
|
+
path: '/',
|
|
485
|
+
headers: {
|
|
486
|
+
'content-type': 'application/json; charset=utf8'
|
|
487
|
+
},
|
|
488
|
+
body: ''
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
t.same(response.statusCode, 415)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test('content-type fail when parameters not match - string 2', async t => {
|
|
495
|
+
t.plan(1)
|
|
496
|
+
|
|
497
|
+
const fastify = Fastify()
|
|
498
|
+
fastify.removeAllContentTypeParsers()
|
|
499
|
+
fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
|
|
500
|
+
t.fail('shouldn\'t be called')
|
|
501
|
+
done(null, body)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
fastify.post('/', async () => {
|
|
505
|
+
return 'ok'
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const response = await fastify.inject({
|
|
509
|
+
method: 'POST',
|
|
510
|
+
path: '/',
|
|
511
|
+
headers: {
|
|
512
|
+
'content-type': 'application/json; charset=utf8; foo=baz'
|
|
513
|
+
},
|
|
514
|
+
body: ''
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
t.same(response.statusCode, 415)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
test('content-type fail when parameters not match - regexp', async t => {
|
|
521
|
+
t.plan(1)
|
|
522
|
+
|
|
523
|
+
const fastify = Fastify()
|
|
524
|
+
fastify.removeAllContentTypeParsers()
|
|
525
|
+
fastify.addContentTypeParser(/application\/json; charset=utf8; foo=bar/, function (request, body, done) {
|
|
526
|
+
t.fail('shouldn\'t be called')
|
|
527
|
+
done(null, body)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
fastify.post('/', async () => {
|
|
531
|
+
return 'ok'
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const response = await fastify.inject({
|
|
535
|
+
method: 'POST',
|
|
536
|
+
path: '/',
|
|
537
|
+
headers: {
|
|
538
|
+
'content-type': 'application/json; charset=utf8'
|
|
539
|
+
},
|
|
540
|
+
body: ''
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
t.same(response.statusCode, 415)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// Refs: https://github.com/fastify/fastify/issues/4495
|
|
547
|
+
test('content-type regexp list should be cloned when plugin override', async t => {
|
|
548
|
+
t.plan(6)
|
|
549
|
+
|
|
550
|
+
const fastify = Fastify()
|
|
551
|
+
|
|
552
|
+
fastify.addContentTypeParser(/^image\/.*/, { parseAs: 'buffer' }, (req, payload, done) => {
|
|
553
|
+
done(null, payload)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
fastify.register(function plugin (fastify, options, done) {
|
|
557
|
+
fastify.post('/', function (request, reply) {
|
|
558
|
+
reply.type(request.headers['content-type']).send(request.body)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
done()
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
{
|
|
565
|
+
const { payload, headers, statusCode } = await fastify.inject({
|
|
566
|
+
method: 'POST',
|
|
567
|
+
path: '/',
|
|
568
|
+
payload: 'jpeg',
|
|
569
|
+
headers: { 'content-type': 'image/jpeg' }
|
|
570
|
+
})
|
|
571
|
+
t.same(statusCode, 200)
|
|
572
|
+
t.same(headers['content-type'], 'image/jpeg')
|
|
573
|
+
t.same(payload, 'jpeg')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
{
|
|
577
|
+
const { payload, headers, statusCode } = await fastify.inject({
|
|
578
|
+
method: 'POST',
|
|
579
|
+
path: '/',
|
|
580
|
+
payload: 'png',
|
|
581
|
+
headers: { 'content-type': 'image/png' }
|
|
582
|
+
})
|
|
583
|
+
t.same(statusCode, 200)
|
|
584
|
+
t.same(headers['content-type'], 'image/png')
|
|
585
|
+
t.same(payload, 'png')
|
|
586
|
+
}
|
|
587
|
+
})
|
|
@@ -1120,7 +1120,7 @@ test('The charset should not interfere with the content type handling', t => {
|
|
|
1120
1120
|
url: 'http://localhost:' + fastify.server.address().port,
|
|
1121
1121
|
body: '{"hello":"world"}',
|
|
1122
1122
|
headers: {
|
|
1123
|
-
'Content-Type': 'application/json charset=utf-8'
|
|
1123
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
1124
1124
|
}
|
|
1125
1125
|
}, (err, response, body) => {
|
|
1126
1126
|
t.error(err)
|
|
@@ -1303,7 +1303,7 @@ test('contentTypeParser should add a custom parser with RegExp value', t => {
|
|
|
1303
1303
|
url: 'http://localhost:' + fastify.server.address().port,
|
|
1304
1304
|
body: '{"hello":"world"}',
|
|
1305
1305
|
headers: {
|
|
1306
|
-
'Content-Type': 'weird
|
|
1306
|
+
'Content-Type': 'weird/content-type+json'
|
|
1307
1307
|
}
|
|
1308
1308
|
}, (err, response, body) => {
|
|
1309
1309
|
t.error(err)
|
|
@@ -1333,7 +1333,7 @@ test('contentTypeParser should add multiple custom parsers with RegExp values',
|
|
|
1333
1333
|
done(null, 'xml')
|
|
1334
1334
|
})
|
|
1335
1335
|
|
|
1336
|
-
fastify.addContentTypeParser(/.*\+myExtension
|
|
1336
|
+
fastify.addContentTypeParser(/.*\+myExtension$/i, function (req, payload, done) {
|
|
1337
1337
|
let data = ''
|
|
1338
1338
|
payload.on('data', chunk => { data += chunk })
|
|
1339
1339
|
payload.on('end', () => {
|