fastify 4.10.0 → 4.10.2

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.
@@ -265,6 +265,8 @@ section.
265
265
  - [`fastify-cockroachdb`](https://github.com/alex-ppg/fastify-cockroachdb)
266
266
  Fastify plugin to connect to a CockroachDB PostgreSQL instance via the
267
267
  Sequelize ORM.
268
+ - [`fastify-constraints`](https://github.com/nearform/fastify-constraints)
269
+ Fastify plugin to add constraints to multiple routes
268
270
  - [`fastify-couchdb`](https://github.com/nigelhanlon/fastify-couchdb) Fastify
269
271
  plugin to add CouchDB support via [nano](https://github.com/apache/nano).
270
272
  - [`fastify-crud-generator`](https://github.com/beliven-it/fastify-crud-generator)
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.10.0'
3
+ const VERSION = '4.10.2'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { AsyncResource } = require('async_hooks')
4
4
  const lru = require('tiny-lru').lru
5
+ // TODO: find more perforamant solution
6
+ const { parse: parseContentType } = require('content-type')
5
7
 
6
8
  const secureJson = require('secure-json-parse')
7
9
  const {
@@ -33,7 +35,7 @@ function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning)
33
35
  this.customParsers = new Map()
34
36
  this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
35
37
  this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
36
- this.parserList = ['application/json', 'text/plain']
38
+ this.parserList = [new ParserListItem('application/json'), new ParserListItem('text/plain')]
37
39
  this.parserRegExpList = []
38
40
  this.cache = lru(100)
39
41
  }
@@ -66,7 +68,7 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
66
68
  this.customParsers.set('', parser)
67
69
  } else {
68
70
  if (contentTypeIsString) {
69
- this.parserList.unshift(contentType)
71
+ this.parserList.unshift(new ParserListItem(contentType))
70
72
  } else {
71
73
  this.parserRegExpList.unshift(contentType)
72
74
  }
@@ -97,11 +99,20 @@ ContentTypeParser.prototype.getParser = function (contentType) {
97
99
  const parser = this.cache.get(contentType)
98
100
  if (parser !== undefined) return parser
99
101
 
102
+ const parsed = safeParseContentType(contentType)
103
+
104
+ // dummyContentType always the same object
105
+ // we can use === for the comparsion and return early
106
+ if (parsed === dummyContentType) {
107
+ return this.customParsers.get('')
108
+ }
109
+
100
110
  // eslint-disable-next-line no-var
101
111
  for (var i = 0; i !== this.parserList.length; ++i) {
102
- const parserName = this.parserList[i]
103
- if (contentType.indexOf(parserName) !== -1) {
104
- const parser = this.customParsers.get(parserName)
112
+ const parserListItem = this.parserList[i]
113
+ if (compareContentType(parsed, parserListItem)) {
114
+ const parser = this.customParsers.get(parserListItem.name)
115
+ // we set request content-type in cache to reduce parsing of MIME type
105
116
  this.cache.set(contentType, parser)
106
117
  return parser
107
118
  }
@@ -110,8 +121,9 @@ ContentTypeParser.prototype.getParser = function (contentType) {
110
121
  // eslint-disable-next-line no-var
111
122
  for (var j = 0; j !== this.parserRegExpList.length; ++j) {
112
123
  const parserRegExp = this.parserRegExpList[j]
113
- if (parserRegExp.test(contentType)) {
124
+ if (compareRegExpContentType(contentType, parsed.type, parserRegExp)) {
114
125
  const parser = this.customParsers.get(parserRegExp.toString())
126
+ // we set request content-type in cache to reduce parsing of MIME type
115
127
  this.cache.set(contentType, parser)
116
128
  return parser
117
129
  }
@@ -346,6 +358,63 @@ function removeAllContentTypeParsers () {
346
358
  this[kContentTypeParser].removeAll()
347
359
  }
348
360
 
361
+ // dummy here to prevent repeated object creation
362
+ const dummyContentType = { type: '', parameters: Object.create(null) }
363
+
364
+ function safeParseContentType (contentType) {
365
+ try {
366
+ return parseContentType(contentType)
367
+ } catch (err) {
368
+ return dummyContentType
369
+ }
370
+ }
371
+
372
+ function compareContentType (contentType, parserListItem) {
373
+ if (parserListItem.isEssence) {
374
+ // we do essence check
375
+ return contentType.type.indexOf(parserListItem) !== -1
376
+ } else {
377
+ // when the content-type includes parameters
378
+ // we do a full-text search
379
+ // reject essence content-type before checking parameters
380
+ if (contentType.type.indexOf(parserListItem.type) === -1) return false
381
+ for (const key of parserListItem.parameterKeys) {
382
+ // reject when missing parameters
383
+ if (!(key in contentType.parameters)) return false
384
+ // reject when parameters do not match
385
+ if (contentType.parameters[key] !== parserListItem.parameters[key]) return false
386
+ }
387
+ return true
388
+ }
389
+ }
390
+
391
+ function compareRegExpContentType (contentType, essenceMIMEType, regexp) {
392
+ if (regexp.source.indexOf(';') === -1) {
393
+ // we do essence check
394
+ return regexp.test(essenceMIMEType)
395
+ } else {
396
+ // when the content-type includes parameters
397
+ // we do a full-text match
398
+ return regexp.test(contentType)
399
+ }
400
+ }
401
+
402
+ function ParserListItem (contentType) {
403
+ this.name = contentType
404
+ // we pre-calculate all the needed information
405
+ // before content-type comparsion
406
+ const parsed = safeParseContentType(contentType)
407
+ this.type = parsed.type
408
+ this.parameters = parsed.parameters
409
+ this.parameterKeys = Object.keys(parsed.parameters)
410
+ this.isEssence = contentType.indexOf(';') === -1
411
+ }
412
+
413
+ // used in ContentTypeParser.remove
414
+ ParserListItem.prototype.toString = function () {
415
+ return this.name
416
+ }
417
+
349
418
  module.exports = ContentTypeParser
350
419
  module.exports.helpers = {
351
420
  buildContentTypeParser,
package/package.json CHANGED
@@ -1,10 +1,32 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.10.0",
3
+ "version": "4.10.2",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
7
7
  "types": "fastify.d.ts",
8
+ "scripts": {
9
+ "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"",
10
+ "benchmark": "npx concurrently -k -s first \"node ./examples/benchmark/simple.js\" \"npx autocannon -c 100 -d 30 -p 10 localhost:3000/\"",
11
+ "coverage": "npm run unit -- --cov --coverage-report=html",
12
+ "coverage:ci": "npm run unit -- --cov --coverage-report=html --no-browser --no-check-coverage -R terse",
13
+ "coverage:ci-check-coverage": "nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100",
14
+ "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause\"",
15
+ "lint": "npm run lint:standard && npm run lint:typescript && npm run lint:markdown",
16
+ "lint:fix": "standard --fix",
17
+ "lint:markdown": "markdownlint-cli2",
18
+ "lint:standard": "standard | snazzy",
19
+ "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts",
20
+ "prepublishOnly": "PREPUBLISH=true tap --no-check-coverage test/build/**.test.js",
21
+ "test": "npm run lint && npm run unit && npm run test:typescript",
22
+ "test:ci": "npm run unit -- -R terse --cov --coverage-report=lcovonly && npm run test:typescript",
23
+ "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
24
+ "test:typescript": "tsc test/types/import.ts && tsd",
25
+ "test:watch": "npm run unit -- -w --no-coverage-report -R terse",
26
+ "unit": "tap",
27
+ "unit:junit": "tap-mocha-reporter xunit < out.tap > test/junit-testresults.xml",
28
+ "unit:report": "tap --cov --coverage-report=html --coverage-report=cobertura | tee out.tap"
29
+ },
8
30
  "repository": {
9
31
  "type": "git",
10
32
  "url": "git+https://github.com/fastify/fastify.git"
@@ -105,7 +127,7 @@
105
127
  "devDependencies": {
106
128
  "@fastify/pre-commit": "^2.0.2",
107
129
  "@sinclair/typebox": "^0.25.2",
108
- "@sinonjs/fake-timers": "^9.1.2",
130
+ "@sinonjs/fake-timers": "^10.0.0",
109
131
  "@types/node": "^18.7.18",
110
132
  "@typescript-eslint/eslint-plugin": "^5.37.0",
111
133
  "@typescript-eslint/parser": "^5.37.0",
@@ -154,6 +176,7 @@
154
176
  "@fastify/fast-json-stringify-compiler": "^4.1.0",
155
177
  "abstract-logging": "^2.0.1",
156
178
  "avvio": "^8.2.0",
179
+ "content-type": "^1.0.4",
157
180
  "find-my-way": "^7.3.0",
158
181
  "light-my-request": "^5.6.1",
159
182
  "pino": "^8.5.0",
@@ -176,26 +199,5 @@
176
199
  },
177
200
  "tsd": {
178
201
  "directory": "test/types"
179
- },
180
- "scripts": {
181
- "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"",
182
- "benchmark": "npx concurrently -k -s first \"node ./examples/benchmark/simple.js\" \"npx autocannon -c 100 -d 30 -p 10 localhost:3000/\"",
183
- "coverage": "npm run unit -- --cov --coverage-report=html",
184
- "coverage:ci": "npm run unit -- --cov --coverage-report=html --no-browser --no-check-coverage -R terse",
185
- "coverage:ci-check-coverage": "nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100",
186
- "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause\"",
187
- "lint": "npm run lint:standard && npm run lint:typescript && npm run lint:markdown",
188
- "lint:fix": "standard --fix",
189
- "lint:markdown": "markdownlint-cli2",
190
- "lint:standard": "standard | snazzy",
191
- "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts",
192
- "test": "npm run lint && npm run unit && npm run test:typescript",
193
- "test:ci": "npm run unit -- -R terse --cov --coverage-report=lcovonly && npm run test:typescript",
194
- "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
195
- "test:typescript": "tsc test/types/import.ts && tsd",
196
- "test:watch": "npm run unit -- -w --no-coverage-report -R terse",
197
- "unit": "tap",
198
- "unit:junit": "tap-mocha-reporter xunit < out.tap > test/junit-testresults.xml",
199
- "unit:report": "tap --cov --coverage-report=html --coverage-report=cobertura | tee out.tap"
200
202
  }
201
- }
203
+ }
@@ -395,3 +395,217 @@ test('Safeguard against malicious content-type / 3', async t => {
395
395
 
396
396
  t.same(response.statusCode, 415)
397
397
  })
398
+
399
+ test('Safeguard against content-type spoofing - string', async t => {
400
+ t.plan(1)
401
+
402
+ const fastify = Fastify()
403
+ fastify.removeAllContentTypeParsers()
404
+ fastify.addContentTypeParser('text/plain', function (request, body, done) {
405
+ t.pass('should be called')
406
+ done(null, body)
407
+ })
408
+ fastify.addContentTypeParser('application/json', function (request, body, done) {
409
+ t.fail('shouldn\'t be called')
410
+ done(null, body)
411
+ })
412
+
413
+ fastify.post('/', async () => {
414
+ return 'ok'
415
+ })
416
+
417
+ await fastify.inject({
418
+ method: 'POST',
419
+ path: '/',
420
+ headers: {
421
+ 'content-type': 'text/plain; content-type="application/json"'
422
+ },
423
+ body: ''
424
+ })
425
+ })
426
+
427
+ test('Safeguard against content-type spoofing - regexp', async t => {
428
+ t.plan(1)
429
+
430
+ const fastify = Fastify()
431
+ fastify.removeAllContentTypeParsers()
432
+ fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {
433
+ t.pass('should be called')
434
+ done(null, body)
435
+ })
436
+ fastify.addContentTypeParser(/application\/json/, function (request, body, done) {
437
+ t.fail('shouldn\'t be called')
438
+ done(null, body)
439
+ })
440
+
441
+ fastify.post('/', async () => {
442
+ return 'ok'
443
+ })
444
+
445
+ await fastify.inject({
446
+ method: 'POST',
447
+ path: '/',
448
+ headers: {
449
+ 'content-type': 'text/plain; content-type="application/json"'
450
+ },
451
+ body: ''
452
+ })
453
+ })
454
+
455
+ test('content-type match parameters - string 1', async t => {
456
+ t.plan(1)
457
+
458
+ const fastify = Fastify()
459
+ fastify.removeAllContentTypeParsers()
460
+ fastify.addContentTypeParser('text/plain; charset=utf8', function (request, body, done) {
461
+ t.fail('shouldn\'t be called')
462
+ done(null, body)
463
+ })
464
+ fastify.addContentTypeParser('application/json; charset=utf8', function (request, body, done) {
465
+ t.pass('should be called')
466
+ done(null, body)
467
+ })
468
+
469
+ fastify.post('/', async () => {
470
+ return 'ok'
471
+ })
472
+
473
+ await fastify.inject({
474
+ method: 'POST',
475
+ path: '/',
476
+ headers: {
477
+ 'content-type': 'application/json; charset=utf8'
478
+ },
479
+ body: ''
480
+ })
481
+ })
482
+
483
+ test('content-type match parameters - string 2', async t => {
484
+ t.plan(1)
485
+
486
+ const fastify = Fastify()
487
+ fastify.removeAllContentTypeParsers()
488
+ fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
489
+ t.pass('should be called')
490
+ done(null, body)
491
+ })
492
+ fastify.addContentTypeParser('text/plain; charset=utf8; foo=bar', function (request, body, done) {
493
+ t.fail('shouldn\'t be called')
494
+ done(null, body)
495
+ })
496
+
497
+ fastify.post('/', async () => {
498
+ return 'ok'
499
+ })
500
+
501
+ await fastify.inject({
502
+ method: 'POST',
503
+ path: '/',
504
+ headers: {
505
+ 'content-type': 'application/json; foo=bar; charset=utf8'
506
+ },
507
+ body: ''
508
+ })
509
+ })
510
+
511
+ test('content-type match parameters - regexp', async t => {
512
+ t.plan(1)
513
+
514
+ const fastify = Fastify()
515
+ fastify.removeAllContentTypeParsers()
516
+ fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {
517
+ t.pass('should be called')
518
+ done(null, body)
519
+ })
520
+
521
+ fastify.post('/', async () => {
522
+ return 'ok'
523
+ })
524
+
525
+ await fastify.inject({
526
+ method: 'POST',
527
+ path: '/',
528
+ headers: {
529
+ 'content-type': 'application/json; charset=utf8'
530
+ },
531
+ body: ''
532
+ })
533
+ })
534
+
535
+ test('content-type fail when parameters not match - string 1', async t => {
536
+ t.plan(1)
537
+
538
+ const fastify = Fastify()
539
+ fastify.removeAllContentTypeParsers()
540
+ fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
541
+ t.fail('shouldn\'t be called')
542
+ done(null, body)
543
+ })
544
+
545
+ fastify.post('/', async () => {
546
+ return 'ok'
547
+ })
548
+
549
+ const response = await fastify.inject({
550
+ method: 'POST',
551
+ path: '/',
552
+ headers: {
553
+ 'content-type': 'application/json; charset=utf8'
554
+ },
555
+ body: ''
556
+ })
557
+
558
+ t.same(response.statusCode, 415)
559
+ })
560
+
561
+ test('content-type fail when parameters not match - string 2', async t => {
562
+ t.plan(1)
563
+
564
+ const fastify = Fastify()
565
+ fastify.removeAllContentTypeParsers()
566
+ fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {
567
+ t.fail('shouldn\'t be called')
568
+ done(null, body)
569
+ })
570
+
571
+ fastify.post('/', async () => {
572
+ return 'ok'
573
+ })
574
+
575
+ const response = await fastify.inject({
576
+ method: 'POST',
577
+ path: '/',
578
+ headers: {
579
+ 'content-type': 'application/json; charset=utf8; foo=baz'
580
+ },
581
+ body: ''
582
+ })
583
+
584
+ t.same(response.statusCode, 415)
585
+ })
586
+
587
+ test('content-type fail when parameters not match - regexp', async t => {
588
+ t.plan(1)
589
+
590
+ const fastify = Fastify()
591
+ fastify.removeAllContentTypeParsers()
592
+ fastify.addContentTypeParser(/application\/json; charset=utf8; foo=bar/, function (request, body, done) {
593
+ t.fail('shouldn\'t be called')
594
+ done(null, body)
595
+ })
596
+
597
+ fastify.post('/', async () => {
598
+ return 'ok'
599
+ })
600
+
601
+ const response = await fastify.inject({
602
+ method: 'POST',
603
+ path: '/',
604
+ headers: {
605
+ 'content-type': 'application/json; charset=utf8'
606
+ },
607
+ body: ''
608
+ })
609
+
610
+ t.same(response.statusCode, 415)
611
+ })
@@ -1053,7 +1053,7 @@ test('The charset should not interfere with the content type handling', t => {
1053
1053
  url: getUrl(fastify),
1054
1054
  body: '{"hello":"world"}',
1055
1055
  headers: {
1056
- 'Content-Type': 'application/json charset=utf-8'
1056
+ 'Content-Type': 'application/json; charset=utf-8'
1057
1057
  }
1058
1058
  }, (err, response, body) => {
1059
1059
  t.error(err)
@@ -1236,7 +1236,7 @@ test('contentTypeParser should add a custom parser with RegExp value', t => {
1236
1236
  url: getUrl(fastify),
1237
1237
  body: '{"hello":"world"}',
1238
1238
  headers: {
1239
- 'Content-Type': 'weird-content-type+json'
1239
+ 'Content-Type': 'weird/content-type+json'
1240
1240
  }
1241
1241
  }, (err, response, body) => {
1242
1242
  t.error(err)
@@ -1266,7 +1266,7 @@ test('contentTypeParser should add multiple custom parsers with RegExp values',
1266
1266
  done(null, 'xml')
1267
1267
  })
1268
1268
 
1269
- fastify.addContentTypeParser(/.*\+myExtension$/, function (req, payload, done) {
1269
+ fastify.addContentTypeParser(/.*\+myExtension$/i, function (req, payload, done) {
1270
1270
  let data = ''
1271
1271
  payload.on('data', chunk => { data += chunk })
1272
1272
  payload.on('end', () => {
@@ -36,19 +36,18 @@ test('listen should accept stringified number port', t => {
36
36
 
37
37
  test('listen should reject string port', async (t) => {
38
38
  t.plan(2)
39
-
40
39
  const fastify = Fastify()
41
40
  t.teardown(fastify.close.bind(fastify))
42
41
 
43
42
  try {
44
43
  await fastify.listen({ port: 'hello-world' })
45
44
  } catch (error) {
46
- t.same(error.message, 'options.port should be >= 0 and < 65536. Received hello-world.')
45
+ t.equal(error.code, 'ERR_SOCKET_BAD_PORT')
47
46
  }
48
47
 
49
48
  try {
50
49
  await fastify.listen({ port: '1234hello' })
51
50
  } catch (error) {
52
- t.same(error.message, 'options.port should be >= 0 and < 65536. Received 1234hello.')
51
+ t.equal(error.code, 'ERR_SOCKET_BAD_PORT')
53
52
  }
54
53
  })
@@ -183,6 +183,7 @@ expectDeprecated({} as FastifyLoggerInstance)
183
183
  const childParent = fastify().log
184
184
  // we test different option variant here
185
185
  expectType<FastifyLoggerInstance>(childParent.child({}, { level: 'info' }))
186
+ expectType<FastifyLoggerInstance>(childParent.child({}, { level: 'silent' }))
186
187
  expectType<FastifyLoggerInstance>(childParent.child({}, { redact: ['pass', 'pin'] }))
187
188
  expectType<FastifyLoggerInstance>(childParent.child({}, { serializers: { key: () => {} } }))
188
189
  expectType<FastifyLoggerInstance>(childParent.child({}, { level: 'info', redact: ['pass', 'pin'], serializers: { key: () => {} } }))
package/types/logger.d.ts CHANGED
@@ -13,7 +13,7 @@ import pino from 'pino'
13
13
  */
14
14
  export type FastifyLogFn = pino.LogFn
15
15
 
16
- export type LogLevel = pino.Level
16
+ export type LogLevel = pino.LevelWithSilent
17
17
 
18
18
  export type Bindings = pino.Bindings
19
19