UNPKG

7.85 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 kRequestPayloadStream,
10 kState,
11 kTestInternals
12} = require('./symbols')
13
14const {
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')
25const warning = require('./warnings')
26
27function 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
36ContentTypeParser.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
69ContentTypeParser.prototype.hasParser = function (contentType) {
70 return contentType in this.customParsers
71}
72
73ContentTypeParser.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
83ContentTypeParser.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
95ContentTypeParser.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
132function 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
207function 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
225function defaultPlainTextParser (req, body, done) {
226 done(null, body)
227}
228
229function Parser (asString, asBuffer, bodyLimit, fn) {
230 this.asString = asString
231 this.asBuffer = asBuffer
232 this.bodyLimit = bodyLimit
233 this.fn = fn
234
235 // Check for deprecation syntax
236 if (fn.length === (fn.constructor.name === 'AsyncFunction' ? 1 : 2)) {
237 warning.emit('FSTDEP003')
238 this.isDeprecatedSignature = true
239 }
240}
241
242function 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
250function 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
272function hasContentTypeParser (contentType) {
273 return this[kContentTypeParser].hasParser(contentType)
274}
275
276module.exports = ContentTypeParser
277module.exports.helpers = {
278 buildContentTypeParser,
279 addContentTypeParser,
280 hasContentTypeParser
281}
282module.exports.defaultParsers = {
283 getDefaultJsonParser,
284 defaultTextParser: defaultPlainTextParser
285}
286module.exports[kTestInternals] = { rawBody }