fastify
Version:
Fast and low overhead web framework, for Node.js
209 lines (178 loc) • 5.87 kB
JavaScript
const lru = require('tiny-lru')
const Bourne = require('bourne')
function ContentTypeParser (bodyLimit, onProtoPoisoning) {
this._defaultJsonParser = getDefaultJsonParser(onProtoPoisoning)
this.customParsers = {}
this.customParsers['application/json'] = new Parser(true, false, bodyLimit, this._defaultJsonParser)
this.parserList = ['application/json']
this.cache = lru(100)
}
ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
if (typeof contentType !== 'string') throw new Error('The content type should be a string')
if (contentType.length === 0) throw new Error('The content type cannot be an empty string')
if (typeof parserFn !== 'function') throw new Error('The content type handler should be a function')
if (this.hasParser(contentType)) {
throw new Error(`Content type parser '${contentType}' already present.`)
}
if (opts.parseAs !== undefined) {
if (opts.parseAs !== 'string' && opts.parseAs !== 'buffer') {
throw new Error(`The body parser can only parse your data as 'string' or 'buffer', you asked '${opts.parseAs}' which is not supported.`)
}
}
const parser = new Parser(
opts.parseAs === 'string',
opts.parseAs === 'buffer',
opts.bodyLimit,
parserFn
)
if (contentType === '*') {
this.parserList.push('')
this.customParsers[''] = parser
} else {
if (contentType !== 'application/json') {
this.parserList.unshift(contentType)
}
this.customParsers[contentType] = parser
}
}
ContentTypeParser.prototype.hasParser = function (contentType) {
if (contentType === 'application/json') {
return this.customParsers['application/json'].fn !== this._defaultJsonParser
}
return contentType in this.customParsers
}
ContentTypeParser.prototype.getParser = function (contentType) {
for (var i = 0; i < this.parserList.length; i++) {
if (contentType.indexOf(this.parserList[i]) > -1) {
var parser = this.customParsers[this.parserList[i]]
this.cache.set(contentType, parser)
return parser
}
}
return this.customParsers['']
}
ContentTypeParser.prototype.run = function (contentType, handler, request, reply) {
var parser = this.cache.get(contentType) || this.getParser(contentType)
if (parser === undefined) {
const err = new Error('Unsupported Media Type: ' + contentType)
err.statusCode = 415
reply.code(err.statusCode).send(err)
} else {
if (parser.asString === true || parser.asBuffer === true) {
rawBody(
request,
reply,
reply.context._parserOptions,
parser,
done
)
} else {
var result = parser.fn(request.raw, done)
if (result && typeof result.then === 'function') {
result.then(body => done(null, body), done)
}
}
}
function done (error, body) {
if (error) {
reply.send(error)
} else {
request.body = body
handler(reply)
}
}
}
function rawBody (request, reply, options, parser, done) {
var asString = parser.asString
var limit = options.limit === null ? parser.bodyLimit : options.limit
var contentLength = request.headers['content-length'] === undefined
? NaN
: Number.parseInt(request.headers['content-length'], 10)
if (contentLength > limit) {
const err = new Error('Request body is too large')
err.statusCode = 413
reply.code(err.statusCode).send(err)
return
}
var receivedLength = 0
var body = asString === true ? '' : []
var req = request.raw
if (asString === true) {
req.setEncoding('utf8')
}
req.on('data', onData)
req.on('end', onEnd)
req.on('error', onEnd)
function onData (chunk) {
receivedLength += chunk.length
if (receivedLength > limit) {
req.removeListener('data', onData)
req.removeListener('end', onEnd)
req.removeListener('error', onEnd)
const err = new Error('Request body is too large')
err.statusCode = 413
reply.code(err.statusCode).send(err)
return
}
if (asString === true) {
body += chunk
} else {
body.push(chunk)
}
}
function onEnd (err) {
req.removeListener('data', onData)
req.removeListener('end', onEnd)
req.removeListener('error', onEnd)
if (err !== undefined) {
err.statusCode = 400
reply.code(err.statusCode).send(err)
return
}
if (asString === true) {
receivedLength = Buffer.byteLength(body)
}
if (!Number.isNaN(contentLength) && receivedLength !== contentLength) {
const err = new Error('Request body size did not match Content-Length')
err.statusCode = 400
reply.code(err.statusCode).send(err)
return
}
if (asString === false) {
body = Buffer.concat(body)
}
var result = parser.fn(req, body, done)
if (result && typeof result.then === 'function') {
result.then(body => done(null, body), done)
}
}
}
function getDefaultJsonParser (onProtoPoisoning) {
return defaultJsonParser
function defaultJsonParser (req, body, done) {
try {
var json = Bourne.parse(body, { protoAction: onProtoPoisoning })
} catch (err) {
err.statusCode = 400
return done(err, undefined)
}
done(null, json)
}
}
function buildContentTypeParser (c) {
const contentTypeParser = new ContentTypeParser()
contentTypeParser._defaultJsonParser = c._defaultJsonParser
Object.assign(contentTypeParser.customParsers, c.customParsers)
contentTypeParser.parserList = c.parserList.slice()
return contentTypeParser
}
function Parser (asString, asBuffer, bodyLimit, fn) {
this.asString = asString
this.asBuffer = asBuffer
this.bodyLimit = bodyLimit
this.fn = fn
}
module.exports = ContentTypeParser
module.exports.buildContentTypeParser = buildContentTypeParser
module.exports[Symbol.for('internals')] = { rawBody }