strong-tie/inbound-calls
0
1'use strict'2 3const { test } = require('node:test')4const Fastify = require('..')5const keys = require('../lib/symbols')6const { FST_ERR_CTP_ALREADY_PRESENT, FST_ERR_CTP_INVALID_TYPE, FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('../lib/errors')7 8const first = function (req, payload, done) {}9const second = function (req, payload, done) {}10const third = function (req, payload, done) {}11 12test('hasContentTypeParser', async t => {13 await t.test('should know about internal parsers', (t, done) => {14 t.plan(5)15 16 const fastify = Fastify()17 fastify.ready(err => {18 t.assert.ifError(err)19 t.assert.ok(fastify.hasContentTypeParser('application/json'))20 t.assert.ok(fastify.hasContentTypeParser('text/plain'))21 t.assert.ok(fastify.hasContentTypeParser(' text/plain '))22 t.assert.ok(!fastify.hasContentTypeParser('application/jsoff'))23 done()24 })25 })26 27 await t.test('should only work with string and RegExp', t => {28 t.plan(8)29 30 const fastify = Fastify()31 fastify.addContentTypeParser(/^image\/.*/, first)32 fastify.addContentTypeParser(/^application\/.+\+xml/, first)33 fastify.addContentTypeParser('image/gif', first)34 35 t.assert.ok(fastify.hasContentTypeParser('application/json'))36 t.assert.ok(fastify.hasContentTypeParser(/^image\/.*/))37 t.assert.ok(fastify.hasContentTypeParser(/^application\/.+\+xml/))38 t.assert.ok(fastify.hasContentTypeParser('image/gif'))39 t.assert.ok(!fastify.hasContentTypeParser(/^image\/.+\+xml/))40 t.assert.ok(!fastify.hasContentTypeParser('image/png'))41 t.assert.ok(!fastify.hasContentTypeParser('*'))42 t.assert.throws(43 () => fastify.hasContentTypeParser(123),44 FST_ERR_CTP_INVALID_TYPE45 )46 })47})48 49test('getParser', async t => {50 await t.test('should return matching parser', t => {51 t.plan(6)52 53 const fastify = Fastify()54 55 fastify.addContentTypeParser(/^image\/.*/, first)56 fastify.addContentTypeParser(/^application\/.+\+xml/, second)57 fastify.addContentTypeParser('text/html', third)58 59 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('application/t+xml').fn, second)60 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/png').fn, first)61 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, third)62 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html; charset=utf-8').fn, third)63 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html ; charset=utf-8').fn, third)64 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/htmlINVALID')?.fn, undefined)65 })66 67 await t.test('should return matching parser with caching /1', t => {68 t.plan(6)69 70 const fastify = Fastify()71 72 fastify.addContentTypeParser('text/html', first)73 74 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)75 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 0)76 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html ').fn, first)77 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)78 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html ').fn, first)79 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)80 })81 82 await t.test('should return matching parser with caching /2', t => {83 t.plan(8)84 85 const fastify = Fastify()86 87 fastify.addContentTypeParser('text/html', first)88 89 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)90 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 0)91 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/HTML').fn, first)92 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)93 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('TEXT/html').fn, first)94 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)95 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('TEXT/html').fn, first)96 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)97 })98 99 await t.test('should return matching parser with caching /3', t => {100 t.plan(6)101 102 const fastify = Fastify()103 104 fastify.addContentTypeParser(/^text\/html(;\s*charset=[^;]+)?$/, first)105 106 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)107 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)108 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html;charset=utf-8').fn, first)109 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)110 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html;charset=utf-8').fn, first)111 t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)112 })113 114 await t.test('should prefer content type parser with string value', t => {115 t.plan(2)116 117 const fastify = Fastify()118 119 fastify.addContentTypeParser(/^image\/.*/, first)120 fastify.addContentTypeParser('image/gif', second)121 122 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/gif').fn, second)123 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/png').fn, first)124 })125 126 await t.test('should return parser that catches all if no other is set', t => {127 t.plan(3)128 129 const fastify = Fastify()130 131 fastify.addContentTypeParser('*', first)132 fastify.addContentTypeParser(/^text\/.*/, second)133 134 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/gif').fn, first)135 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, second)136 t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text').fn, first)137 })138 139 await t.test('should return undefined if no matching parser exist', t => {140 t.plan(2)141 142 const fastify = Fastify()143 144 fastify.addContentTypeParser(/^weirdType\/.+/, first)145 fastify.addContentTypeParser('application/javascript', first)146 147 t.assert.ok(!fastify[keys.kContentTypeParser].getParser('application/xml'))148 t.assert.ok(!fastify[keys.kContentTypeParser].getParser('weirdType/'))149 })150})151 152test('existingParser', async t => {153 await t.test('returns always false for "*"', t => {154 t.plan(2)155 156 const fastify = Fastify()157 158 fastify.addContentTypeParser(/^image\/.*/, first)159 fastify.addContentTypeParser(/^application\/.+\+xml/, first)160 fastify.addContentTypeParser('text/html', first)161 162 t.assert.ok(!fastify[keys.kContentTypeParser].existingParser('*'))163 164 fastify.addContentTypeParser('*', first)165 166 t.assert.ok(!fastify[keys.kContentTypeParser].existingParser('*'))167 })168 169 await t.test('let you override the default parser once', t => {170 t.plan(2)171 172 const fastify = Fastify()173 174 fastify.addContentTypeParser('application/json', first)175 fastify.addContentTypeParser('text/plain', first)176 177 t.assert.throws(178 () => fastify.addContentTypeParser('application/json', first),179 FST_ERR_CTP_ALREADY_PRESENT180 )181 t.assert.throws(182 () => fastify.addContentTypeParser('text/plain', first),183 FST_ERR_CTP_ALREADY_PRESENT184 )185 })186 187 const fastify = Fastify()188 const contentTypeParser = fastify[keys.kContentTypeParser]189 190 fastify.addContentTypeParser(/^image\/.*/, first)191 fastify.addContentTypeParser(/^application\/.+\+xml/, first)192 fastify.addContentTypeParser('text/html', first)193 194 t.assert.ok(contentTypeParser.existingParser(/^image\/.*/))195 t.assert.ok(contentTypeParser.existingParser('text/html'))196 t.assert.ok(contentTypeParser.existingParser(/^application\/.+\+xml/))197 t.assert.ok(!contentTypeParser.existingParser('application/json'))198 t.assert.ok(!contentTypeParser.existingParser('text/plain'))199 t.assert.ok(!contentTypeParser.existingParser('image/png'))200 t.assert.ok(!contentTypeParser.existingParser(/^application\/.+\+json/))201})202 203test('add', async t => {204 await t.test('should only accept string and RegExp', t => {205 t.plan(4)206 207 const fastify = Fastify()208 const contentTypeParser = fastify[keys.kContentTypeParser]209 210 t.assert.ifError(contentTypeParser.add('test', {}, first))211 t.assert.ifError(contentTypeParser.add(/test/, {}, first))212 t.assert.throws(213 () => contentTypeParser.add({}, {}, first),214 FST_ERR_CTP_INVALID_TYPE,215 'The content type should be a string or a RegExp'216 )217 t.assert.throws(218 () => contentTypeParser.add(1, {}, first),219 FST_ERR_CTP_INVALID_TYPE,220 'The content type should be a string or a RegExp'221 )222 })223 224 await t.test('should set "*" as parser that catches all', t => {225 t.plan(1)226 227 const fastify = Fastify()228 const contentTypeParser = fastify[keys.kContentTypeParser]229 230 contentTypeParser.add('*', {}, first)231 t.assert.strictEqual(contentTypeParser.customParsers.get('').fn, first)232 })233 234 await t.test('should lowercase contentTypeParser name', async t => {235 t.plan(1)236 const fastify = Fastify()237 fastify.addContentTypeParser('text/html', function (req, done) {238 done()239 })240 try {241 fastify.addContentTypeParser('TEXT/html', function (req, done) {242 done()243 })244 } catch (err) {245 t.assert.strictEqual(err.message, FST_ERR_CTP_ALREADY_PRESENT('text/html').message)246 }247 })248 249 await t.test('should trim contentTypeParser name', async t => {250 t.plan(1)251 const fastify = Fastify()252 fastify.addContentTypeParser('text/html', function (req, done) {253 done()254 })255 try {256 fastify.addContentTypeParser(' text/html', function (req, done) {257 done()258 })259 } catch (err) {260 t.assert.strictEqual(err.message, FST_ERR_CTP_ALREADY_PRESENT('text/html').message)261 }262 })263})264 265test('non-Error thrown from content parser is properly handled', (t, done) => {266 t.plan(3)267 268 const fastify = Fastify()269 270 const throwable = 'test'271 const payload = 'error'272 273 fastify.addContentTypeParser('text/test', (request, payload, done) => {274 done(throwable)275 })276 277 fastify.post('/', (req, reply) => {278 })279 280 fastify.setErrorHandler((err, req, res) => {281 t.assert.strictEqual(err, throwable)282 283 res.send(payload)284 })285 286 fastify.inject({287 method: 'POST',288 url: '/',289 headers: { 'Content-Type': 'text/test' },290 body: 'some text'291 }, (err, res) => {292 t.assert.ifError(err)293 t.assert.strictEqual(res.payload, payload)294 done()295 })296})297 298test('Error thrown 415 from content type is null and make post request to server', (t, done) => {299 t.plan(3)300 301 const fastify = Fastify()302 const errMsg = new FST_ERR_CTP_INVALID_MEDIA_TYPE(undefined).message303 304 fastify.post('/', (req, reply) => {305 })306 307 fastify.inject({308 method: 'POST',309 url: '/',310 body: 'some text'311 }, (err, res) => {312 t.assert.ifError(err)313 t.assert.strictEqual(res.statusCode, 415)314 t.assert.strictEqual(JSON.parse(res.body).message, errMsg)315 done()316 })317})318 319test('remove', async t => {320 await t.test('should remove default parser', t => {321 t.plan(6)322 323 const fastify = Fastify()324 const contentTypeParser = fastify[keys.kContentTypeParser]325 326 t.assert.ok(contentTypeParser.remove('application/json'))327 t.assert.ok(!contentTypeParser.customParsers['application/json'])328 t.assert.ok(!contentTypeParser.parserList.find(parser => parser === 'application/json'))329 t.assert.ok(contentTypeParser.remove(' text/plain '))330 t.assert.ok(!contentTypeParser.customParsers['text/plain'])331 t.assert.ok(!contentTypeParser.parserList.find(parser => parser === 'text/plain'))332 })333 334 await t.test('should remove RegExp parser', t => {335 t.plan(3)336 337 const fastify = Fastify()338 fastify.addContentTypeParser(/^text\/*/, first)339 340 const contentTypeParser = fastify[keys.kContentTypeParser]341 342 t.assert.ok(contentTypeParser.remove(/^text\/*/))343 t.assert.ok(!contentTypeParser.customParsers[/^text\/*/])344 t.assert.ok(!contentTypeParser.parserRegExpList.find(parser => parser.toString() === /^text\/*/.toString()))345 })346 347 await t.test('should throw an error if content type is neither string nor RegExp', t => {348 t.plan(1)349 350 const fastify = Fastify()351 352 t.assert.throws(() => fastify[keys.kContentTypeParser].remove(12), FST_ERR_CTP_INVALID_TYPE)353 })354 355 await t.test('should return false if content type does not exist', t => {356 t.plan(1)357 358 const fastify = Fastify()359 360 t.assert.ok(!fastify[keys.kContentTypeParser].remove('image/png'))361 })362 363 await t.test('should not remove any content type parser if content type does not exist', t => {364 t.plan(2)365 366 const fastify = Fastify()367 368 const contentTypeParser = fastify[keys.kContentTypeParser]369 370 t.assert.ok(!contentTypeParser.remove('image/png'))371 t.assert.strictEqual(contentTypeParser.customParsers.size, 2)372 })373})374 375test('remove all should remove all existing parsers and reset cache', t => {376 t.plan(4)377 378 const fastify = Fastify()379 fastify.addContentTypeParser('application/xml', first)380 fastify.addContentTypeParser(/^image\/.*/, first)381 382 const contentTypeParser = fastify[keys.kContentTypeParser]383 384 contentTypeParser.getParser('application/xml') // fill cache with one entry385 contentTypeParser.removeAll()386 387 t.assert.strictEqual(contentTypeParser.cache.size, 0)388 t.assert.strictEqual(contentTypeParser.parserList.length, 0)389 t.assert.strictEqual(contentTypeParser.parserRegExpList.length, 0)390 t.assert.strictEqual(Object.keys(contentTypeParser.customParsers).length, 0)391})392 393test('Safeguard against malicious content-type / 1', async t => {394 const badNames = Object.getOwnPropertyNames({}.__proto__) // eslint-disable-line395 t.plan(badNames.length)396 397 const fastify = Fastify()398 399 fastify.post('/', async () => {400 return 'ok'401 })402 403 for (const prop of badNames) {404 const response = await fastify.inject({405 method: 'POST',406 path: '/',407 headers: {408 'content-type': prop409 },410 body: ''411 })412 413 t.assert.strictEqual(response.statusCode, 415)414 }415})416 417test('Safeguard against malicious content-type / 2', async t => {418 t.plan(1)419 420 const fastify = Fastify()421 422 fastify.post('/', async () => {423 return 'ok'424 })425 426 const response = await fastify.inject({427 method: 'POST',428 path: '/',429 headers: {430 'content-type': '\\u0063\\u006fnstructor'431 },432 body: ''433 })434 435 t.assert.strictEqual(response.statusCode, 415)436})437 438test('Safeguard against malicious content-type / 3', async t => {439 t.plan(1)440 441 const fastify = Fastify()442 443 fastify.post('/', async () => {444 return 'ok'445 })446 447 const response = await fastify.inject({448 method: 'POST',449 path: '/',450 headers: {451 'content-type': 'constructor; charset=utf-8'452 },453 body: ''454 })455 456 t.assert.strictEqual(response.statusCode, 415)457})458 459test('Safeguard against content-type spoofing - string', async t => {460 t.plan(1)461 462 const fastify = Fastify()463 fastify.removeAllContentTypeParsers()464 fastify.addContentTypeParser('text/plain', function (request, body, done) {465 t.assert.ok('should be called')466 done(null, body)467 })468 fastify.addContentTypeParser('application/json', function (request, body, done) {469 t.assert.fail('shouldn\'t be called')470 done(null, body)471 })472 473 fastify.post('/', async () => {474 return 'ok'475 })476 477 await fastify.inject({478 method: 'POST',479 path: '/',480 headers: {481 'content-type': 'text/plain; content-type="application/json"'482 },483 body: ''484 })485})486 487test('Warning against improper content-type - regexp', async t => {488 await t.test('improper regex - text plain', (t, done) => {489 t.plan(2)490 const fastify = Fastify()491 492 process.on('warning', onWarning)493 function onWarning (warning) {494 t.assert.strictEqual(warning.name, 'FastifySecurity')495 t.assert.strictEqual(warning.code, 'FSTSEC001')496 done()497 }498 t.after(() => process.removeListener('warning', onWarning))499 500 fastify.removeAllContentTypeParsers()501 fastify.addContentTypeParser(/text\/plain/, function (request, body, done) {502 done(null, body)503 })504 })505 506 await t.test('improper regex - application json', (t, done) => {507 t.plan(2)508 const fastify = Fastify()509 510 process.on('warning', onWarning)511 function onWarning (warning) {512 t.assert.strictEqual(warning.name, 'FastifySecurity')513 t.assert.strictEqual(warning.code, 'FSTSEC001')514 done()515 }516 t.after(() => process.removeListener('warning', onWarning))517 518 fastify.removeAllContentTypeParsers()519 520 fastify.addContentTypeParser(/application\/json/, function (request, body, done) {521 done(null, body)522 })523 })524})525 526test('content-type match parameters - string 1', async t => {527 t.plan(1)528 529 const fastify = Fastify()530 fastify.removeAllContentTypeParsers()531 fastify.addContentTypeParser('text/plain; charset=utf8', function (request, body, done) {532 t.assert.fail('shouldn\'t be called')533 done(null, body)534 })535 fastify.addContentTypeParser('application/json; charset=utf8', function (request, body, done) {536 t.assert.ok('should be called')537 done(null, body)538 })539 540 fastify.post('/', async () => {541 return 'ok'542 })543 544 await fastify.inject({545 method: 'POST',546 path: '/',547 headers: {548 'content-type': 'application/json; charset=utf8'549 },550 body: ''551 })552})553 554test('content-type match parameters - regexp', async t => {555 t.plan(1)556 557 const fastify = Fastify()558 fastify.removeAllContentTypeParsers()559 fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {560 t.assert.ok('should be called')561 done(null, body)562 })563 564 fastify.post('/', async () => {565 return 'ok'566 })567 568 await fastify.inject({569 method: 'POST',570 path: '/',571 headers: {572 'content-type': 'application/json; charset=utf8'573 },574 body: ''575 })576})577 578test('content-type fail when parameters not match - string 1', async t => {579 t.plan(1)580 581 const fastify = Fastify()582 fastify.removeAllContentTypeParsers()583 fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {584 t.assert.fail('shouldn\'t be called')585 done(null, body)586 })587 588 fastify.post('/', async () => {589 return 'ok'590 })591 592 const response = await fastify.inject({593 method: 'POST',594 path: '/',595 headers: {596 'content-type': 'application/json; charset=utf8'597 },598 body: ''599 })600 601 t.assert.strictEqual(response.statusCode, 415)602})603 604test('content-type fail when parameters not match - string 2', async t => {605 t.plan(1)606 607 const fastify = Fastify()608 fastify.removeAllContentTypeParsers()609 fastify.addContentTypeParser('application/json; charset=utf8; foo=bar', function (request, body, done) {610 t.assert.fail('shouldn\'t be called')611 done(null, body)612 })613 614 fastify.post('/', async () => {615 return 'ok'616 })617 618 const response = await fastify.inject({619 method: 'POST',620 path: '/',621 headers: {622 'content-type': 'application/json; charset=utf8; foo=baz'623 },624 body: ''625 })626 627 t.assert.strictEqual(response.statusCode, 415)628})629 630test('content-type fail when parameters not match - regexp', async t => {631 t.plan(1)632 633 const fastify = Fastify()634 fastify.removeAllContentTypeParsers()635 fastify.addContentTypeParser(/application\/json; charset=utf8; foo=bar/, function (request, body, done) {636 t.assert.fail('shouldn\'t be called')637 done(null, body)638 })639 640 fastify.post('/', async () => {641 return 'ok'642 })643 644 const response = await fastify.inject({645 method: 'POST',646 path: '/',647 headers: {648 'content-type': 'application/json; charset=utf8'649 },650 body: ''651 })652 653 t.assert.strictEqual(response.statusCode, 415)654})655 656// Refs: https://github.com/fastify/fastify/issues/4495657test('content-type regexp list should be cloned when plugin override', async t => {658 t.plan(6)659 660 const fastify = Fastify()661 662 fastify.addContentTypeParser(/^image\/.*/, { parseAs: 'buffer' }, (req, payload, done) => {663 done(null, payload)664 })665 666 fastify.register(function plugin (fastify, options, done) {667 fastify.post('/', function (request, reply) {668 reply.type(request.headers['content-type']).send(request.body)669 })670 671 done()672 })673 674 {675 const { payload, headers, statusCode } = await fastify.inject({676 method: 'POST',677 path: '/',678 payload: 'jpeg',679 headers: { 'content-type': 'image/jpeg' }680 })681 t.assert.strictEqual(statusCode, 200)682 t.assert.strictEqual(headers['content-type'], 'image/jpeg')683 t.assert.strictEqual(payload, 'jpeg')684 }685 686 {687 const { payload, headers, statusCode } = await fastify.inject({688 method: 'POST',689 path: '/',690 payload: 'png',691 headers: { 'content-type': 'image/png' }692 })693 t.assert.strictEqual(statusCode, 200)694 t.assert.strictEqual(headers['content-type'], 'image/png')695 t.assert.strictEqual(payload, 'png')696 }697})698 699test('edge case content-type - ;', async t => {700 t.plan(1)701 702 const fastify = Fastify()703 fastify.removeAllContentTypeParsers()704 fastify.addContentTypeParser(';', function (request, body, done) {705 t.assert.fail('should not be called')706 done(null, body)707 })708 709 fastify.post('/', async () => {710 return 'ok'711 })712 713 await fastify.inject({714 method: 'POST',715 path: '/',716 headers: {717 'content-type': 'application/json; foo=bar; charset=utf8'718 },719 body: ''720 })721 722 await fastify.inject({723 method: 'POST',724 path: '/',725 headers: {726 'content-type': 'image/jpeg'727 },728 body: ''729 })730 731 t.assert.ok('end')732})733 