You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
5.3 KiB
152 lines
5.3 KiB
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 = <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> {
|
|
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<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 ( !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<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) => { // 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
|
|
}
|
|
}
|