UNPKG

7.1 kBJavaScriptView Raw
1'use strict'
2
3const lru = require('tiny-lru')
4const secureJson = require('secure-json-parse')
5const {
6 kDefaultJsonParse,
7 kContentTypeParser,
8 kBodyLimit,
9 kState
10} = require('./symbols')
11const {
12 codes: {
13 FST_ERR_CTP_INVALID_TYPE,
14 FST_ERR_CTP_EMPTY_TYPE,
15 FST_ERR_CTP_ALREADY_PRESENT,
16 FST_ERR_CTP_INVALID_HANDLER,
17 FST_ERR_CTP_INVALID_PARSE_TYPE,
18 FST_ERR_CTP_BODY_TOO_LARGE,
19 FST_ERR_CTP_INVALID_MEDIA_TYPE,
20 FST_ERR_CTP_INVALID_CONTENT_LENGTH,
21 FST_ERR_CTP_EMPTY_JSON_BODY
22 }
23} = require('./errors')
24
25function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning) {
26 this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning)
27 this.customParsers = {}
28 this.customParsers['application/json'] = new Parser(true, false, bodyLimit, this[kDefaultJsonParse])
29 this.customParsers['text/plain'] = new Parser(true, false, bodyLimit, defaultPlainTextParser)
30 this.parserList = ['application/json', 'text/plain']
31 this.cache = lru(100)
32}
33
34ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
35 if (typeof contentType !== 'string') throw new FST_ERR_CTP_INVALID_TYPE()
36 if (contentType.length === 0) throw new FST_ERR_CTP_EMPTY_TYPE()
37 if (typeof parserFn !== 'function') throw new FST_ERR_CTP_INVALID_HANDLER()
38
39 if (this.hasParser(contentType)) {
40 throw new FST_ERR_CTP_ALREADY_PRESENT(contentType)
41 }
42
43 if (opts.parseAs !== undefined) {
44 if (opts.parseAs !== 'string' && opts.parseAs !== 'buffer') {
45 throw new FST_ERR_CTP_INVALID_PARSE_TYPE(opts.parseAs)
46 }
47 }
48
49 const parser = new Parser(
50 opts.parseAs === 'string',
51 opts.parseAs === 'buffer',
52 opts.bodyLimit,
53 parserFn
54 )
55
56 if (contentType === '*') {
57 this.parserList.push('')
58 this.customParsers[''] = parser
59 } else {
60 if (contentType !== 'application/json') {
61 this.parserList.unshift(contentType)
62 }
63 this.customParsers[contentType] = parser
64 }
65}
66
67ContentTypeParser.prototype.hasParser = function (contentType) {
68 if (contentType === 'application/json') {
69 return this.customParsers['application/json'].fn !== this[kDefaultJsonParse]
70 }
71 if (contentType === 'text/plain') {
72 return this.customParsers['text/plain'].fn !== defaultPlainTextParser
73 }
74 return contentType in this.customParsers
75}
76
77ContentTypeParser.prototype.getParser = function (contentType) {
78 for (var i = 0; i < this.parserList.length; i++) {
79 if (contentType.indexOf(this.parserList[i]) > -1) {
80 var parser = this.customParsers[this.parserList[i]]
81 this.cache.set(contentType, parser)
82 return parser
83 }
84 }
85
86 return this.customParsers['']
87}
88
89ContentTypeParser.prototype.run = function (contentType, handler, request, reply) {
90 var parser = this.cache.get(contentType) || this.getParser(contentType)
91 if (parser === undefined) {
92 reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType))
93 } else {
94 if (parser.asString === true || parser.asBuffer === true) {
95 rawBody(
96 request,
97 reply,
98 reply.context._parserOptions,
99 parser,
100 done
101 )
102 } else {
103 var result = parser.fn(request.raw, done)
104 if (result && typeof result.then === 'function') {
105 result.then(body => done(null, body), done)
106 }
107 }
108 }
109
110 function done (error, body) {
111 if (error) {
112 reply.send(error)
113 } else {
114 request.body = body
115 handler(request, reply)
116 }
117 }
118}
119
120function rawBody (request, reply, options, parser, done) {
121 var asString = parser.asString
122 var limit = options.limit === null ? parser.bodyLimit : options.limit
123 var contentLength = request.headers['content-length'] === undefined
124 ? NaN
125 : Number.parseInt(request.headers['content-length'], 10)
126
127 if (contentLength > limit) {
128 reply.send(new FST_ERR_CTP_BODY_TOO_LARGE())
129 return
130 }
131
132 var receivedLength = 0
133 var body = asString === true ? '' : []
134 var req = request.raw
135
136 if (asString === true) {
137 req.setEncoding('utf8')
138 }
139
140 req.on('data', onData)
141 req.on('end', onEnd)
142 req.on('error', onEnd)
143
144 function onData (chunk) {
145 receivedLength += chunk.length
146
147 if (receivedLength > limit) {
148 req.removeListener('data', onData)
149 req.removeListener('end', onEnd)
150 req.removeListener('error', onEnd)
151 reply.send(new FST_ERR_CTP_BODY_TOO_LARGE())
152 return
153 }
154
155 if (asString === true) {
156 body += chunk
157 } else {
158 body.push(chunk)
159 }
160 }
161
162 function onEnd (err) {
163 req.removeListener('data', onData)
164 req.removeListener('end', onEnd)
165 req.removeListener('error', onEnd)
166
167 if (err !== undefined) {
168 err.statusCode = 400
169 reply.code(err.statusCode).send(err)
170 return
171 }
172
173 if (asString === true) {
174 receivedLength = Buffer.byteLength(body)
175 }
176 if (!Number.isNaN(contentLength) && receivedLength !== contentLength) {
177 reply.send(new FST_ERR_CTP_INVALID_CONTENT_LENGTH())
178 return
179 }
180
181 if (asString === false) {
182 body = Buffer.concat(body)
183 }
184
185 var result = parser.fn(req, body, done)
186 if (result && typeof result.then === 'function') {
187 result.then(body => done(null, body), done)
188 }
189 }
190}
191
192function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
193 return defaultJsonParser
194
195 function defaultJsonParser (req, body, done) {
196 if (body === '' || body == null) {
197 return done(new FST_ERR_CTP_EMPTY_JSON_BODY(), undefined)
198 }
199
200 try {
201 var json = secureJson.parse(body, { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning })
202 } catch (err) {
203 err.statusCode = 400
204 return done(err, undefined)
205 }
206 done(null, json)
207 }
208}
209
210function defaultPlainTextParser (req, body, done) {
211 done(null, body)
212}
213
214function Parser (asString, asBuffer, bodyLimit, fn) {
215 this.asString = asString
216 this.asBuffer = asBuffer
217 this.bodyLimit = bodyLimit
218 this.fn = fn
219}
220
221function buildContentTypeParser (c) {
222 const contentTypeParser = new ContentTypeParser()
223 contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
224 Object.assign(contentTypeParser.customParsers, c.customParsers)
225 contentTypeParser.parserList = c.parserList.slice()
226 return contentTypeParser
227}
228
229function addContentTypeParser (contentType, opts, parser) {
230 if (this[kState].started) {
231 throw new Error('Cannot call "addContentTypeParser" when fastify instance is already started!')
232 }
233
234 if (typeof opts === 'function') {
235 parser = opts
236 opts = {}
237 }
238
239 if (!opts) opts = {}
240 if (!opts.bodyLimit) opts.bodyLimit = this[kBodyLimit]
241
242 if (Array.isArray(contentType)) {
243 contentType.forEach((type) => this[kContentTypeParser].add(type, opts, parser))
244 } else {
245 this[kContentTypeParser].add(contentType, opts, parser)
246 }
247
248 return this
249}
250
251function hasContentTypeParser (contentType) {
252 return this[kContentTypeParser].hasParser(contentType)
253}
254
255module.exports = ContentTypeParser
256module.exports.helpers = {
257 buildContentTypeParser,
258 addContentTypeParser,
259 hasContentTypeParser
260}
261module.exports[Symbol.for('internals')] = { rawBody }