1 | 'use strict'
|
2 |
|
3 | const lru = require('tiny-lru')
|
4 | const Bourne = require('bourne')
|
5 | const {
|
6 | kDefaultJsonParse,
|
7 | kContentTypeParser,
|
8 | kBodyLimit,
|
9 | kState
|
10 | } = require('./symbols')
|
11 | const {
|
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 |
|
25 | function ContentTypeParser (bodyLimit, onProtoPoisoning) {
|
26 | this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning)
|
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 |
|
34 | ContentTypeParser.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 |
|
67 | ContentTypeParser.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 |
|
77 | ContentTypeParser.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 |
|
89 | ContentTypeParser.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 |
|
120 | function 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 |
|
192 | function getDefaultJsonParser (onProtoPoisoning) {
|
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 = Bourne.parse(body, { protoAction: onProtoPoisoning })
|
202 | } catch (err) {
|
203 | err.statusCode = 400
|
204 | return done(err, undefined)
|
205 | }
|
206 | done(null, json)
|
207 | }
|
208 | }
|
209 |
|
210 | function defaultPlainTextParser (req, body, done) {
|
211 | done(null, body)
|
212 | }
|
213 |
|
214 | function Parser (asString, asBuffer, bodyLimit, fn) {
|
215 | this.asString = asString
|
216 | this.asBuffer = asBuffer
|
217 | this.bodyLimit = bodyLimit
|
218 | this.fn = fn
|
219 | }
|
220 |
|
221 | function 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 |
|
229 | function 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 |
|
251 | function hasContentTypeParser (contentType) {
|
252 | return this[kContentTypeParser].hasParser(contentType)
|
253 | }
|
254 |
|
255 | module.exports = ContentTypeParser
|
256 | module.exports.helpers = {
|
257 | buildContentTypeParser,
|
258 | addContentTypeParser,
|
259 | hasContentTypeParser
|
260 | }
|
261 | module.exports[Symbol.for('internals')] = { rawBody }
|