2021-03-31 02:15:39 +00:00
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 ] )
2021-04-12 16:43:06 +00:00
. filter ( Boolean ) . map ( x = > x ! . toLowerCase ( ) . split ( ';' ) [ 0 ] )
2021-04-10 10:09:32 +00:00
if ( ! contentType ) return request
2021-03-31 02:15:39 +00:00
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
}
2021-04-12 16:43:06 +00:00
/ * *
* 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
}
2021-04-12 16:43:06 +00:00
/ * *
* 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 > {
2021-03-31 02:15:39 +00:00
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
}
}