import util from 'util' import Busboy from 'busboy' import { WriteStream } from 'fs-capacitor' import createError from 'http-errors' import objectPath from 'object-path' import { SPEC_URL } from './constants' import { ignoreStream } from './ignoreStream' import { isEnumerableObject } from './isEnumerableObject' class Upload { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = file => { this.file = file resolve(file) } this.reject = reject }) this.promise.catch(() => {}) } } export const processRequest = ( request, response, { maxFieldSize = 1000000, maxFileSize = Infinity, maxFiles = Infinity } = {} ) => new Promise((resolve, reject) => { let released let exitError let currentStream let operations let operationsPath let map const parser = new Busboy({ headers: request.headers, limits: { fieldSize: maxFieldSize, fields: 2, fileSize: maxFileSize, files: maxFiles } }) const exit = error => { if (exitError) return exitError = error reject(exitError) parser.destroy() if (currentStream) currentStream.destroy(exitError) if (map) for (const upload of map.values()) if (!upload.file) upload.reject(exitError) request.unpipe(parser) setImmediate(() => { request.resume() }) } const release = () => { // istanbul ignore next if (released) return released = true if (map) for (const upload of map.values()) if (upload.file) upload.file.capacitor.destroy() } const abort = () => { exit( createError( 499, 'Request disconnected during file upload stream parsing.' ) ) } parser.on( 'field', (fieldName, value, fieldNameTruncated, valueTruncated) => { if (exitError) return if (valueTruncated) return exit( createError( 413, `The ‘${fieldName}’ multipart field value exceeds the ${maxFieldSize} byte size limit.` ) ) switch (fieldName) { case 'operations': try { operations = JSON.parse(value) } catch (error) { return exit( createError( 400, `Invalid JSON in the ‘operations’ multipart field (${SPEC_URL}).` ) ) } if (!isEnumerableObject(operations) && !Array.isArray(operations)) return exit( createError( 400, `Invalid type for the ‘operations’ multipart field (${SPEC_URL}).` ) ) operationsPath = objectPath(operations) break case 'map': { if (!operations) return exit( createError( 400, `Misordered multipart fields; ‘map’ should follow ‘operations’ (${SPEC_URL}).` ) ) let parsedMap try { parsedMap = JSON.parse(value) } catch (error) { return exit( createError( 400, `Invalid JSON in the ‘map’ multipart field (${SPEC_URL}).` ) ) } if (!isEnumerableObject(parsedMap)) return exit( createError( 400, `Invalid type for the ‘map’ multipart field (${SPEC_URL}).` ) ) const mapEntries = Object.entries(parsedMap) if (mapEntries.length > maxFiles) return exit( createError(413, `${maxFiles} max file uploads exceeded.`) ) map = new Map() for (const [fieldName, paths] of mapEntries) { if (!Array.isArray(paths)) return exit( createError( 400, `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array (${SPEC_URL}).` ) ) map.set(fieldName, new Upload()) for (const [index, path] of paths.entries()) { if (typeof path !== 'string') return exit( createError( 400, `Invalid type for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value (${SPEC_URL}).` ) ) try { operationsPath.set(path, map.get(fieldName).promise) } catch (error) { return exit( createError( 400, `Invalid object path for the ‘map’ multipart field entry key ‘${fieldName}’ array index ‘${index}’ value ‘${path}’ (${SPEC_URL}).` ) ) } } } resolve(operations) } } } ) parser.on('file', (fieldName, stream, filename, encoding, mimetype) => { if (exitError) { ignoreStream(stream) return } if (!map) { ignoreStream(stream) return exit( createError( 400, `Misordered multipart fields; files should follow ‘map’ (${SPEC_URL}).` ) ) } currentStream = stream stream.on('end', () => { currentStream = null }) const upload = map.get(fieldName) if (!upload) { ignoreStream(stream) return } const capacitor = new WriteStream() capacitor.on('error', () => { stream.unpipe() stream.resume() }) stream.on('limit', () => { stream.unpipe() capacitor.destroy( createError( 413, `File truncated as it exceeds the ${maxFileSize} byte size limit.` ) ) }) stream.on('error', error => { stream.unpipe() // istanbul ignore next capacitor.destroy(exitError || error) }) stream.pipe(capacitor) const file = { filename, mimetype, encoding, createReadStream() { const error = capacitor.error || (released ? exitError : null) if (error) throw error return capacitor.createReadStream() } } let capacitorStream Object.defineProperty(file, 'stream', { get: util.deprecate(function() { if (!capacitorStream) capacitorStream = this.createReadStream() return capacitorStream }, 'File upload property ‘stream’ is deprecated. Use ‘createReadStream()’ instead.') }) Object.defineProperty(file, 'capacitor', { value: capacitor }) upload.resolve(file) }) parser.once('filesLimit', () => exit(createError(413, `${maxFiles} max file uploads exceeded.`)) ) parser.once('finish', () => { request.unpipe(parser) request.resume() if (!operations) return exit( createError( 400, `Missing multipart field ‘operations’ (${SPEC_URL}).` ) ) if (!map) return exit( createError(400, `Missing multipart field ‘map’ (${SPEC_URL}).`) ) for (const upload of map.values()) if (!upload.file) upload.reject(createError(400, 'File missing in the request.')) }) parser.once('error', exit) response.once('finish', release) response.once('close', release) request.once('close', abort) request.once('end', () => { request.removeListener('close', abort) }) request.pipe(parser) })