processRequest.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import util from 'util'
  2. import Busboy from 'busboy'
  3. import { WriteStream } from 'fs-capacitor'
  4. import createError from 'http-errors'
  5. import objectPath from 'object-path'
  6. import { SPEC_URL } from './constants'
  7. import { ignoreStream } from './ignoreStream'
  8. import { isEnumerableObject } from './isEnumerableObject'
  9. class Upload {
  10. constructor() {
  11. this.promise = new Promise((resolve, reject) => {
  12. this.resolve = file => {
  13. this.file = file
  14. resolve(file)
  15. }
  16. this.reject = reject
  17. })
  18. this.promise.catch(() => {})
  19. }
  20. }
  21. export const processRequest = (
  22. request,
  23. response,
  24. { maxFieldSize = 1000000, maxFileSize = Infinity, maxFiles = Infinity } = {}
  25. ) =>
  26. new Promise((resolve, reject) => {
  27. let released
  28. let exitError
  29. let currentStream
  30. let operations
  31. let operationsPath
  32. let map
  33. const parser = new Busboy({
  34. headers: request.headers,
  35. limits: {
  36. fieldSize: maxFieldSize,
  37. fields: 2,
  38. fileSize: maxFileSize,
  39. files: maxFiles
  40. }
  41. })
  42. const exit = error => {
  43. if (exitError) return
  44. exitError = error
  45. reject(exitError)
  46. parser.destroy()
  47. if (currentStream) currentStream.destroy(exitError)
  48. if (map)
  49. for (const upload of map.values())
  50. if (!upload.file) upload.reject(exitError)
  51. request.unpipe(parser)
  52. setImmediate(() => {
  53. request.resume()
  54. })
  55. }
  56. const release = () => {
  57. // istanbul ignore next
  58. if (released) return
  59. released = true
  60. if (map)
  61. for (const upload of map.values())
  62. if (upload.file) upload.file.capacitor.destroy()
  63. }
  64. const abort = () => {
  65. exit(
  66. createError(
  67. 499,
  68. 'Request disconnected during file upload stream parsing.'
  69. )
  70. )
  71. }
  72. parser.on(
  73. 'field',
  74. (fieldName, value, fieldNameTruncated, valueTruncated) => {
  75. if (exitError) return
  76. if (valueTruncated)
  77. return exit(
  78. createError(
  79. 413,
  80. `The ‘${fieldName}’ multipart field value exceeds the ${maxFieldSize} byte size limit.`
  81. )
  82. )
  83. switch (fieldName) {
  84. case 'operations':
  85. try {
  86. operations = JSON.parse(value)
  87. } catch (error) {
  88. return exit(
  89. createError(
  90. 400,
  91. `Invalid JSON in the ‘operations’ multipart field (${SPEC_URL}).`
  92. )
  93. )
  94. }
  95. if (!isEnumerableObject(operations) && !Array.isArray(operations))
  96. return exit(
  97. createError(
  98. 400,
  99. `Invalid type for the ‘operations’ multipart field (${SPEC_URL}).`
  100. )
  101. )
  102. operationsPath = objectPath(operations)
  103. break
  104. case 'map': {
  105. if (!operations)
  106. return exit(
  107. createError(
  108. 400,
  109. `Misordered multipart fields; ‘map’ should follow ‘operations’ (${SPEC_URL}).`
  110. )
  111. )
  112. let parsedMap
  113. try {
  114. parsedMap = JSON.parse(value)
  115. } catch (error) {
  116. return exit(
  117. createError(
  118. 400,
  119. `Invalid JSON in the ‘map’ multipart field (${SPEC_URL}).`
  120. )
  121. )
  122. }
  123. if (!isEnumerableObject(parsedMap))
  124. return exit(
  125. createError(
  126. 400,
  127. `Invalid type for the ‘map’ multipart field (${SPEC_URL}).`
  128. )
  129. )
  130. const mapEntries = Object.entries(parsedMap)
  131. if (mapEntries.length > maxFiles)
  132. return exit(
  133. createError(413, `${maxFiles} max file uploads exceeded.`)
  134. )
  135. map = new Map()
  136. for (const [fieldName, paths] of mapEntries) {
  137. if (!Array.isArray(paths))
  138. return exit(
  139. createError(
  140. 400,
  141. `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array (${SPEC_URL}).`
  142. )
  143. )
  144. map.set(fieldName, new Upload())
  145. for (const [index, path] of paths.entries()) {
  146. if (typeof path !== 'string')
  147. return exit(
  148. createError(
  149. 400,
  150. `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value (${SPEC_URL}).`
  151. )
  152. )
  153. try {
  154. operationsPath.set(path, map.get(fieldName).promise)
  155. } catch (error) {
  156. return exit(
  157. createError(
  158. 400,
  159. `Invalid object path for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value ‘${path}’ (${SPEC_URL}).`
  160. )
  161. )
  162. }
  163. }
  164. }
  165. resolve(operations)
  166. }
  167. }
  168. }
  169. )
  170. parser.on('file', (fieldName, stream, filename, encoding, mimetype) => {
  171. if (exitError) {
  172. ignoreStream(stream)
  173. return
  174. }
  175. if (!map) {
  176. ignoreStream(stream)
  177. return exit(
  178. createError(
  179. 400,
  180. `Misordered multipart fields; files should follow ‘map’ (${SPEC_URL}).`
  181. )
  182. )
  183. }
  184. currentStream = stream
  185. stream.on('end', () => {
  186. currentStream = null
  187. })
  188. const upload = map.get(fieldName)
  189. if (!upload) {
  190. ignoreStream(stream)
  191. return
  192. }
  193. const capacitor = new WriteStream()
  194. capacitor.on('error', () => {
  195. stream.unpipe()
  196. stream.resume()
  197. })
  198. stream.on('limit', () => {
  199. stream.unpipe()
  200. capacitor.destroy(
  201. createError(
  202. 413,
  203. `File truncated as it exceeds the ${maxFileSize} byte size limit.`
  204. )
  205. )
  206. })
  207. stream.on('error', error => {
  208. stream.unpipe() // istanbul ignore next
  209. capacitor.destroy(exitError || error)
  210. })
  211. stream.pipe(capacitor)
  212. const file = {
  213. filename,
  214. mimetype,
  215. encoding,
  216. createReadStream() {
  217. const error = capacitor.error || (released ? exitError : null)
  218. if (error) throw error
  219. return capacitor.createReadStream()
  220. }
  221. }
  222. let capacitorStream
  223. Object.defineProperty(file, 'stream', {
  224. get: util.deprecate(function() {
  225. if (!capacitorStream) capacitorStream = this.createReadStream()
  226. return capacitorStream
  227. }, 'File upload property ‘stream’ is deprecated. Use ‘createReadStream()’ instead.')
  228. })
  229. Object.defineProperty(file, 'capacitor', {
  230. value: capacitor
  231. })
  232. upload.resolve(file)
  233. })
  234. parser.once('filesLimit', () =>
  235. exit(createError(413, `${maxFiles} max file uploads exceeded.`))
  236. )
  237. parser.once('finish', () => {
  238. request.unpipe(parser)
  239. request.resume()
  240. if (!operations)
  241. return exit(
  242. createError(
  243. 400,
  244. `Missing multipart field ‘operations’ (${SPEC_URL}).`
  245. )
  246. )
  247. if (!map)
  248. return exit(
  249. createError(400, `Missing multipart field ‘map’ (${SPEC_URL}).`)
  250. )
  251. for (const upload of map.values())
  252. if (!upload.file)
  253. upload.reject(createError(400, 'File missing in the request.'))
  254. })
  255. parser.once('error', exit)
  256. response.once('finish', release)
  257. response.once('close', release)
  258. request.once('close', abort)
  259. request.once('end', () => {
  260. request.removeListener('close', abort)
  261. })
  262. request.pipe(parser)
  263. })