diff --git a/HISTORY.md b/HISTORY.md index 17dd110e..31ba1c42 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +unreleased +========================= + +* refactor: move common request validation to read function + 2.2.0 / 2025-03-27 ========================= diff --git a/README.md b/README.md index 9fcd4c6f..7fe2e569 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ object after the middleware (i.e. `req.body`). The `json` function takes an optional `options` object that may contain any of the following keys: +##### defaultCharset + +Specify the default character set for the json content if the charset is not +specified in the `Content-Type` header of the request. Defaults to `utf-8`. + ##### inflate When set to `true`, then deflated (compressed) bodies will be inflated; when @@ -291,7 +296,7 @@ Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 form. Defaults to `false`. -#### depth +##### depth The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. diff --git a/lib/read.js b/lib/read.js index eee8b111..b3f2345f 100644 --- a/lib/read.js +++ b/lib/read.js @@ -16,6 +16,8 @@ var getBody = require('raw-body') var iconv = require('iconv-lite') var onFinished = require('on-finished') var zlib = require('node:zlib') +var hasBody = require('type-is').hasBody +var { getCharset } = require('./utils') /** * Module exports. @@ -36,14 +38,52 @@ module.exports = read */ function read (req, res, next, parse, debug, options) { + if (onFinished.isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!options.shouldParse(req)) { + debug('skip parsing') + next() + return + } + + var encoding = null + if (options?.skipCharset !== true) { + encoding = getCharset(req) || options.defaultCharset + + // validate charset + if (!!options?.isValidCharset && !options.isValidCharset(encoding)) { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding, + type: 'charset.unsupported' + })) + return + } + } + var length var opts = options var stream // read options - var encoding = opts.encoding !== null - ? opts.encoding - : null var verify = opts.verify try { diff --git a/lib/types/json.js b/lib/types/json.js index 078ce710..2d136e50 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -12,12 +12,9 @@ * @private */ -var createError = require('http-errors') var debug = require('debug')('body-parser:json') -var isFinished = require('on-finished').isFinished var read = require('../read') -var typeis = require('type-is') -var { getCharset, normalizeOptions } = require('../utils') +var { normalizeOptions } = require('../utils') /** * Module exports. @@ -51,7 +48,7 @@ var JSON_SYNTAX_REGEXP = /#+/g */ function json (options) { - var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/json') + var normalizedOptions = normalizeOptions(options, 'application/json') var reviver = options?.reviver var strict = options?.strict !== false @@ -84,49 +81,11 @@ function json (options) { } return function jsonParser (req, res, next) { - if (isFinished(req)) { - debug('body already parsed') - next() - return - } - - if (!('body' in req)) { - req.body = undefined - } - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // assert charset per RFC 7159 sec 8.1 - var charset = getCharset(req) || 'utf-8' - if (charset.slice(0, 4) !== 'utf-') { - debug('invalid charset') - next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { - charset: charset, - type: 'charset.unsupported' - })) - return - } - - // read read(req, res, next, parse, debug, { - encoding: charset, - inflate, - limit, - verify + ...normalizedOptions, + + // assert charset per RFC 7159 sec 8.1 + isValidCharset: (charset) => charset.slice(0, 4) === 'utf-' }) } } diff --git a/lib/types/raw.js b/lib/types/raw.js index 3788ff27..95ba5817 100644 --- a/lib/types/raw.js +++ b/lib/types/raw.js @@ -11,9 +11,7 @@ */ var debug = require('debug')('body-parser:raw') -var isFinished = require('on-finished').isFinished var read = require('../read') -var typeis = require('type-is') var { normalizeOptions } = require('../utils') /** @@ -31,45 +29,18 @@ module.exports = raw */ function raw (options) { - var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/octet-stream') + var normalizedOptions = normalizeOptions(options, 'application/octet-stream') function parse (buf) { return buf } return function rawParser (req, res, next) { - if (isFinished(req)) { - debug('body already parsed') - next() - return - } - - if (!('body' in req)) { - req.body = undefined - } - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // read read(req, res, next, parse, debug, { - encoding: null, - inflate, - limit, - verify + ...normalizedOptions, + + // Skip charset validation and parse the body as is + skipCharset: true }) } } diff --git a/lib/types/text.js b/lib/types/text.js index 3e0ab1bb..aa096a98 100644 --- a/lib/types/text.js +++ b/lib/types/text.js @@ -11,10 +11,8 @@ */ var debug = require('debug')('body-parser:text') -var isFinished = require('on-finished').isFinished var read = require('../read') -var typeis = require('type-is') -var { getCharset, normalizeOptions } = require('../utils') +var { normalizeOptions } = require('../utils') /** * Module exports. @@ -31,50 +29,13 @@ module.exports = text */ function text (options) { - var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'text/plain') - - var defaultCharset = options?.defaultCharset || 'utf-8' + var normalizedOptions = normalizeOptions(options, 'text/plain') function parse (buf) { return buf } return function textParser (req, res, next) { - if (isFinished(req)) { - debug('body already parsed') - next() - return - } - - if (!('body' in req)) { - req.body = undefined - } - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // get charset - var charset = getCharset(req) || defaultCharset - - // read - read(req, res, next, parse, debug, { - encoding: charset, - inflate, - limit, - verify - }) + read(req, res, next, parse, debug, normalizedOptions) } } diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index f993425e..4d8750aa 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -14,11 +14,9 @@ var createError = require('http-errors') var debug = require('debug')('body-parser:urlencoded') -var isFinished = require('on-finished').isFinished var read = require('../read') -var typeis = require('type-is') var qs = require('qs') -var { getCharset, normalizeOptions } = require('../utils') +var { normalizeOptions } = require('../utils') /** * Module exports. @@ -35,10 +33,9 @@ module.exports = urlencoded */ function urlencoded (options) { - var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/x-www-form-urlencoded') + var normalizedOptions = normalizeOptions(options, 'application/x-www-form-urlencoded') - var defaultCharset = options?.defaultCharset || 'utf-8' - if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') { + if (normalizedOptions.defaultCharset !== 'utf-8' && normalizedOptions.defaultCharset !== 'iso-8859-1') { throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1') } @@ -52,49 +49,11 @@ function urlencoded (options) { } return function urlencodedParser (req, res, next) { - if (isFinished(req)) { - debug('body already parsed') - next() - return - } - - if (!('body' in req)) { - req.body = undefined - } - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // assert charset - var charset = getCharset(req) || defaultCharset - if (charset !== 'utf-8' && charset !== 'iso-8859-1') { - debug('invalid charset') - next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { - charset: charset, - type: 'charset.unsupported' - })) - return - } - - // read read(req, res, next, parse, debug, { - encoding: charset, - inflate, - limit, - verify + ...normalizedOptions, + + // assert charset + isValidCharset: (charset) => charset === 'utf-8' || charset === 'iso-8859-1' }) } } diff --git a/lib/utils.js b/lib/utils.js index eee5d952..c457aa65 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -64,6 +64,7 @@ function normalizeOptions (options, defaultType) { : options?.limit var type = options?.type || defaultType var verify = options?.verify || false + var defaultCharset = options?.defaultCharset || 'utf-8' if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -78,6 +79,7 @@ function normalizeOptions (options, defaultType) { inflate, limit, verify, + defaultCharset, shouldParse } } diff --git a/test/utils.js b/test/utils.js index ee6df5d8..364d3838 100644 --- a/test/utils.js +++ b/test/utils.js @@ -10,6 +10,7 @@ describe('normalizeOptions(options, defaultType)', () => { assert.strictEqual(result.inflate, true) assert.strictEqual(result.limit, 100 * 1024) // 100kb in bytes assert.strictEqual(result.verify, false) + assert.strictEqual(result.defaultCharset, 'utf-8') assert.strictEqual(typeof result.shouldParse, 'function') } }) @@ -19,12 +20,14 @@ describe('normalizeOptions(options, defaultType)', () => { inflate: false, limit: '200kb', type: 'application/xml', - verify: () => {} + verify: () => {}, + defaultCharset: 'iso-8859-1' } const result = normalizeOptions(options, 'application/json') assert.strictEqual(result.inflate, false) assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes assert.strictEqual(result.verify, options.verify) + assert.strictEqual(result.defaultCharset, 'iso-8859-1') assert.strictEqual(typeof result.shouldParse, 'function') }) @@ -41,6 +44,7 @@ describe('normalizeOptions(options, defaultType)', () => { assert.strictEqual(result.inflate, false) assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes assert.strictEqual(result.verify, options.verify) + assert.strictEqual(result.defaultCharset, 'utf-8') assert.strictEqual(typeof result.shouldParse, 'function') assert.strictEqual(result.additional, undefined) assert.strictEqual(result.something, undefined) @@ -109,6 +113,18 @@ describe('normalizeOptions(options, defaultType)', () => { assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json' } }), true) }) }) + + describe('defaultCharset', () => { + it('should return "utf-8" if defaultCharset is not provided', () => { + const result = normalizeOptions({}, 'application/json') + assert.strictEqual(result.defaultCharset, 'utf-8') + }) + + it('should accept a defaultCharset', () => { + const result = normalizeOptions({ defaultCharset: 'iso-8859-1' }, 'application/json') + assert.strictEqual(result.defaultCharset, 'iso-8859-1') + }) + }) }) describe('defaultType', () => {