import {HTTPKernelModule} from '../HTTPKernelModule' import {HTTPKernel} from '../HTTPKernel' import * as Busboy from 'busboy' import {Request} from '../../lifecycle/Request' import {infer, uuid4} from '../../../util' import {Files} from '../../../service/Files' import {Config} from '../../../service/Config' import {Logging} from '../../../service/Logging' import {Injectable, Inject, Container} from '../../../di' @Injectable() export class ParseIncomingBodyHTTPModule extends HTTPKernelModule { static register(kernel: HTTPKernel): void { const files = Container.getContainer().make(Files) const logging = Container.getContainer().make(Logging) if ( !files.hasFilesystem() ) { logging.warn(`No default filesystem is configured. This means request files will not be uploaded.`) } kernel.register(this).first() } @Inject() protected readonly files!: Files @Inject() protected readonly config!: Config @Inject() protected readonly logging!: Logging public async apply(request: Request): Promise { const contentType = request.getHeader('content-type') const contentTypes = (Array.isArray(contentType) ? contentType : [contentType]) .filter(Boolean).map(x => String(x).toLowerCase() .split(';')[0]) if ( !contentType ) { return request } if ( contentTypes.includes('multipart/form-data') || contentTypes.includes('application/x-www-form-urlencoded') ) { return this.applyBusboy(request) } if ( contentTypes.includes('application/json') ) { return this.applyJSON(request) } return request } /** * Parse the request body as JSON. * @param request */ public async applyJSON(request: Request): Promise { await new Promise((res, rej) => { let data = '' request.toNative().on('data', chunk => { data += chunk }) request.toNative().on('end', () => { try { const body = JSON.parse(data) for ( const key in body ) { if ( !Object.prototype.hasOwnProperty.call(body, key) ) { continue } request.parsedInput[key] = body[key] } res() } catch (e) { rej(e) } }) }) return request } /** * Parse the request body using Busboy. This assumes the request contents are multipart. * @param request */ public async applyBusboy(request: Request): Promise { const config = this.config.get('server.uploads', {}) await new Promise((res, rej) => { const busboy = new Busboy({ headers: request.toNative().headers, }) busboy.on('field', (field, val) => { request.parsedInput[field] = infer(val) }) busboy.on('file', async (field, file, filename, encoding, mimetype) => { // eslint-disable-line max-params if ( !this.files.hasFilesystem() ) { return } if ( !config?.enable ) { this.logging.warn(`Skipping uploaded file '${filename}' because uploading is disabled. Set the server.uploads.enable config to allow uploads.`) file.resume() return } if ( config?.filter?.mimetype ) { const rex = new RegExp(config.filter.mimetype) if ( !rex.test(mimetype) ) { this.logging.debug(`Skipping uploaded file '${filename}' because the mimetype does not match the configured filter: (${config.filter.mimetype} -> ${mimetype})`) file.resume() return } } if ( config?.filter?.filename ) { const rex = new RegExp(config.filter.filename) if ( !rex.test(filename) ) { this.logging.debug(`Skipping uploaded file '${filename}' because the file name does not match the configured filter: (${config.filter.filename} -> ${filename})`) file.resume() return } } const fs = this.files.getFilesystem() const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid4()}` this.logging.verbose(`Uploading file in field ${field} to ${fs.getPrefix()}${storePath}`) file.pipe(await fs.putStoreFileAsStream({ storePath })) // FIXME might need to revisit this to ensure we don't res() before pipe finishes request.uploadedFiles[field] = request.parsedInput[field] = fs.getPath(storePath) }) busboy.on('finish', () => { res() }) busboy.on('error', rej) request.toNative().pipe(busboy) }) return request } }