read.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*!
  2. * body-parser
  3. * Copyright(c) 2014-2015 Douglas Christopher Wilson
  4. * MIT Licensed
  5. */
  6. 'use strict'
  7. /**
  8. * Module dependencies.
  9. * @private
  10. */
  11. var createError = require('http-errors')
  12. var getBody = require('raw-body')
  13. var iconv = require('iconv-lite')
  14. var onFinished = require('on-finished')
  15. var zlib = require('node:zlib')
  16. var hasBody = require('type-is').hasBody
  17. var { getCharset } = require('./utils')
  18. /**
  19. * Module exports.
  20. */
  21. module.exports = read
  22. /**
  23. * Read a request into a buffer and parse.
  24. *
  25. * @param {object} req
  26. * @param {object} res
  27. * @param {function} next
  28. * @param {function} parse
  29. * @param {function} debug
  30. * @param {object} options
  31. * @private
  32. */
  33. function read (req, res, next, parse, debug, options) {
  34. if (onFinished.isFinished(req)) {
  35. debug('body already parsed')
  36. next()
  37. return
  38. }
  39. if (!('body' in req)) {
  40. req.body = undefined
  41. }
  42. // skip requests without bodies
  43. if (!hasBody(req)) {
  44. debug('skip empty body')
  45. next()
  46. return
  47. }
  48. debug('content-type %j', req.headers['content-type'])
  49. // determine if request should be parsed
  50. if (!options.shouldParse(req)) {
  51. debug('skip parsing')
  52. next()
  53. return
  54. }
  55. var encoding = null
  56. if (options?.skipCharset !== true) {
  57. encoding = getCharset(req) || options.defaultCharset
  58. // validate charset
  59. if (!!options?.isValidCharset && !options.isValidCharset(encoding)) {
  60. debug('invalid charset')
  61. next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
  62. charset: encoding,
  63. type: 'charset.unsupported'
  64. }))
  65. return
  66. }
  67. }
  68. var length
  69. var opts = options
  70. var stream
  71. // read options
  72. var verify = opts.verify
  73. try {
  74. // get the content stream
  75. stream = contentstream(req, debug, opts.inflate)
  76. length = stream.length
  77. stream.length = undefined
  78. } catch (err) {
  79. return next(err)
  80. }
  81. // set raw-body options
  82. opts.length = length
  83. opts.encoding = verify
  84. ? null
  85. : encoding
  86. // assert charset is supported
  87. if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
  88. return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
  89. charset: encoding.toLowerCase(),
  90. type: 'charset.unsupported'
  91. }))
  92. }
  93. // read body
  94. debug('read body')
  95. getBody(stream, opts, function (error, body) {
  96. if (error) {
  97. var _error
  98. if (error.type === 'encoding.unsupported') {
  99. // echo back charset
  100. _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
  101. charset: encoding.toLowerCase(),
  102. type: 'charset.unsupported'
  103. })
  104. } else {
  105. // set status code on error
  106. _error = createError(400, error)
  107. }
  108. // unpipe from stream and destroy
  109. if (stream !== req) {
  110. req.unpipe()
  111. stream.destroy()
  112. }
  113. // read off entire request
  114. dump(req, function onfinished () {
  115. next(createError(400, _error))
  116. })
  117. return
  118. }
  119. // verify
  120. if (verify) {
  121. try {
  122. debug('verify body')
  123. verify(req, res, body, encoding)
  124. } catch (err) {
  125. next(createError(403, err, {
  126. body: body,
  127. type: err.type || 'entity.verify.failed'
  128. }))
  129. return
  130. }
  131. }
  132. // parse
  133. var str = body
  134. try {
  135. debug('parse body')
  136. str = typeof body !== 'string' && encoding !== null
  137. ? iconv.decode(body, encoding)
  138. : body
  139. req.body = parse(str, encoding)
  140. } catch (err) {
  141. next(createError(400, err, {
  142. body: str,
  143. type: err.type || 'entity.parse.failed'
  144. }))
  145. return
  146. }
  147. next()
  148. })
  149. }
  150. /**
  151. * Get the content stream of the request.
  152. *
  153. * @param {object} req
  154. * @param {function} debug
  155. * @param {boolean} [inflate=true]
  156. * @return {object}
  157. * @api private
  158. */
  159. function contentstream (req, debug, inflate) {
  160. var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
  161. var length = req.headers['content-length']
  162. debug('content-encoding "%s"', encoding)
  163. if (inflate === false && encoding !== 'identity') {
  164. throw createError(415, 'content encoding unsupported', {
  165. encoding: encoding,
  166. type: 'encoding.unsupported'
  167. })
  168. }
  169. if (encoding === 'identity') {
  170. req.length = length
  171. return req
  172. }
  173. var stream = createDecompressionStream(encoding, debug)
  174. req.pipe(stream)
  175. return stream
  176. }
  177. /**
  178. * Create a decompression stream for the given encoding.
  179. * @param {string} encoding
  180. * @param {function} debug
  181. * @return {object}
  182. * @api private
  183. */
  184. function createDecompressionStream (encoding, debug) {
  185. switch (encoding) {
  186. case 'deflate':
  187. debug('inflate body')
  188. return zlib.createInflate()
  189. case 'gzip':
  190. debug('gunzip body')
  191. return zlib.createGunzip()
  192. case 'br':
  193. debug('brotli decompress body')
  194. return zlib.createBrotliDecompress()
  195. default:
  196. throw createError(415, 'unsupported content encoding "' + encoding + '"', {
  197. encoding: encoding,
  198. type: 'encoding.unsupported'
  199. })
  200. }
  201. }
  202. /**
  203. * Dump the contents of a request.
  204. *
  205. * @param {object} req
  206. * @param {function} callback
  207. * @api private
  208. */
  209. function dump (req, callback) {
  210. if (onFinished.isFinished(req)) {
  211. callback(null)
  212. } else {
  213. onFinished(req, callback)
  214. req.resume()
  215. }
  216. }