lib/src/http/kernel/module/ParseIncomingBodyHTTPModule.ts

145 lines
5.2 KiB
TypeScript
Raw Normal View History

import {HTTPKernelModule} from "../HTTPKernelModule"
import {HTTPKernel} from "../HTTPKernel"
import * as Busboy from "busboy"
import {Request} from "../../lifecycle/Request"
import {infer, uuid_v4} from "@extollo/util"
import {Files} from "../../../service/Files"
import {Config} from "../../../service/Config"
import {Logging} from "../../../service/Logging"
import {Injectable, Inject, Container} from "@extollo/di"
@Injectable()
export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
static register(kernel: HTTPKernel) {
const files = <Files> Container.getContainer().make(Files)
const logging = <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<Request> {
2021-04-10 10:09:32 +00:00
const contentType = request.getHeader('content-type')
const contentTypes = (Array.isArray(contentType) ? contentType : [contentType])
.filter(Boolean).map(x => x!.toLowerCase().split(';')[0])
2021-04-10 10:09:32 +00:00
if ( !contentType ) return request
2021-04-10 10:09:32 +00:00
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
*/
2021-04-10 10:09:32 +00:00
public async applyJSON(request: Request): Promise<Request> {
await new Promise<void>((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 ( !body.hasOwnProperty(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
*/
2021-04-10 10:09:32 +00:00
public async applyBusboy(request: Request): Promise<Request> {
const config = this.config.get('server.uploads', {})
await new Promise<void>((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) => {
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}-${uuid_v4()}`
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', () => {
this.logging.debug(`Parsed body input: ${JSON.stringify(request.parsedInput)}`)
res()
})
busboy.on('error', rej)
request.toNative().pipe(busboy)
})
return request
}
}