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