Initial commit
[yaffs-website] / node_modules / raw-body / index.js
1 /*!
2  * raw-body
3  * Copyright(c) 2013-2014 Jonathan Ong
4  * Copyright(c) 2014-2015 Douglas Christopher Wilson
5  * MIT Licensed
6  */
7
8 'use strict'
9
10 /**
11  * Module dependencies.
12  * @private
13  */
14
15 var bytes = require('bytes')
16 var iconv = require('iconv-lite')
17 var unpipe = require('unpipe')
18
19 /**
20  * Module exports.
21  * @public
22  */
23
24 module.exports = getRawBody
25
26 /**
27  * Module variables.
28  * @private
29  */
30
31 var iconvEncodingMessageRegExp = /^Encoding not recognized: /
32
33 /**
34  * Get the decoder for a given encoding.
35  *
36  * @param {string} encoding
37  * @private
38  */
39
40 function getDecoder (encoding) {
41   if (!encoding) return null
42
43   try {
44     return iconv.getDecoder(encoding)
45   } catch (e) {
46     // error getting decoder
47     if (!iconvEncodingMessageRegExp.test(e.message)) throw e
48
49     // the encoding was not found
50     throw createError(415, 'specified encoding unsupported', 'encoding.unsupported', {
51       encoding: encoding
52     })
53   }
54 }
55
56 /**
57  * Get the raw body of a stream (typically HTTP).
58  *
59  * @param {object} stream
60  * @param {object|string|function} [options]
61  * @param {function} [callback]
62  * @public
63  */
64
65 function getRawBody (stream, options, callback) {
66   var done = callback
67   var opts = options || {}
68
69   if (options === true || typeof options === 'string') {
70     // short cut for encoding
71     opts = {
72       encoding: options
73     }
74   }
75
76   if (typeof options === 'function') {
77     done = options
78     opts = {}
79   }
80
81   // validate callback is a function, if provided
82   if (done !== undefined && typeof done !== 'function') {
83     throw new TypeError('argument callback must be a function')
84   }
85
86   // require the callback without promises
87   if (!done && !global.Promise) {
88     throw new TypeError('argument callback is required')
89   }
90
91   // get encoding
92   var encoding = opts.encoding !== true
93     ? opts.encoding
94     : 'utf-8'
95
96   // convert the limit to an integer
97   var limit = bytes.parse(opts.limit)
98
99   // convert the expected length to an integer
100   var length = opts.length != null && !isNaN(opts.length)
101     ? parseInt(opts.length, 10)
102     : null
103
104   if (done) {
105     // classic callback style
106     return readStream(stream, encoding, length, limit, done)
107   }
108
109   return new Promise(function executor (resolve, reject) {
110     readStream(stream, encoding, length, limit, function onRead (err, buf) {
111       if (err) return reject(err)
112       resolve(buf)
113     })
114   })
115 }
116
117 /**
118  * Halt a stream.
119  *
120  * @param {Object} stream
121  * @private
122  */
123
124 function halt (stream) {
125   // unpipe everything from the stream
126   unpipe(stream)
127
128   // pause stream
129   if (typeof stream.pause === 'function') {
130     stream.pause()
131   }
132 }
133
134 /**
135  * Make a serializable error object.
136  *
137  * To create serializable errors you must re-set message so
138  * that it is enumerable and you must re configure the type
139  * property so that is writable and enumerable.
140  *
141  * @param {number} status
142  * @param {string} message
143  * @param {string} type
144  * @param {object} props
145  * @private
146  */
147
148 function createError (status, message, type, props) {
149   var error = new Error()
150
151   // capture stack trace
152   Error.captureStackTrace(error, createError)
153
154   // set free-form properties
155   for (var prop in props) {
156     error[prop] = props[prop]
157   }
158
159   // set message
160   error.message = message
161
162   // set status
163   error.status = status
164   error.statusCode = status
165
166   // set type
167   Object.defineProperty(error, 'type', {
168     value: type,
169     enumerable: true,
170     writable: true,
171     configurable: true
172   })
173
174   return error
175 }
176
177 /**
178  * Read the data from the stream.
179  *
180  * @param {object} stream
181  * @param {string} encoding
182  * @param {number} length
183  * @param {number} limit
184  * @param {function} callback
185  * @public
186  */
187
188 function readStream (stream, encoding, length, limit, callback) {
189   var complete = false
190   var sync = true
191
192   // check the length and limit options.
193   // note: we intentionally leave the stream paused,
194   // so users should handle the stream themselves.
195   if (limit !== null && length !== null && length > limit) {
196     return done(createError(413, 'request entity too large', 'entity.too.large', {
197       expected: length,
198       length: length,
199       limit: limit
200     }))
201   }
202
203   // streams1: assert request encoding is buffer.
204   // streams2+: assert the stream encoding is buffer.
205   //   stream._decoder: streams1
206   //   state.encoding: streams2
207   //   state.decoder: streams2, specifically < 0.10.6
208   var state = stream._readableState
209   if (stream._decoder || (state && (state.encoding || state.decoder))) {
210     // developer error
211     return done(createError(500, 'stream encoding should not be set', 'stream.encoding.set'))
212   }
213
214   var received = 0
215   var decoder
216
217   try {
218     decoder = getDecoder(encoding)
219   } catch (err) {
220     return done(err)
221   }
222
223   var buffer = decoder
224     ? ''
225     : []
226
227   // attach listeners
228   stream.on('aborted', onAborted)
229   stream.on('close', cleanup)
230   stream.on('data', onData)
231   stream.on('end', onEnd)
232   stream.on('error', onEnd)
233
234   // mark sync section complete
235   sync = false
236
237   function done () {
238     var args = new Array(arguments.length)
239
240     // copy arguments
241     for (var i = 0; i < args.length; i++) {
242       args[i] = arguments[i]
243     }
244
245     // mark complete
246     complete = true
247
248     if (sync) {
249       process.nextTick(invokeCallback)
250     } else {
251       invokeCallback()
252     }
253
254     function invokeCallback () {
255       cleanup()
256
257       if (args[0]) {
258         // halt the stream on error
259         halt(stream)
260       }
261
262       callback.apply(null, args)
263     }
264   }
265
266   function onAborted () {
267     if (complete) return
268
269     done(createError(400, 'request aborted', 'request.aborted', {
270       code: 'ECONNABORTED',
271       expected: length,
272       length: length,
273       received: received
274     }))
275   }
276
277   function onData (chunk) {
278     if (complete) return
279
280     received += chunk.length
281     decoder
282       ? buffer += decoder.write(chunk)
283       : buffer.push(chunk)
284
285     if (limit !== null && received > limit) {
286       done(createError(413, 'request entity too large', 'entity.too.large', {
287         limit: limit,
288         received: received
289       }))
290     }
291   }
292
293   function onEnd (err) {
294     if (complete) return
295     if (err) return done(err)
296
297     if (length !== null && received !== length) {
298       done(createError(400, 'request size did not match content length', 'request.size.invalid', {
299         expected: length,
300         length: length,
301         received: received
302       }))
303     } else {
304       var string = decoder
305         ? buffer + (decoder.end() || '')
306         : Buffer.concat(buffer)
307       done(null, string)
308     }
309   }
310
311   function cleanup () {
312     buffer = null
313
314     stream.removeListener('aborted', onAborted)
315     stream.removeListener('data', onData)
316     stream.removeListener('end', onEnd)
317     stream.removeListener('error', onEnd)
318     stream.removeListener('close', cleanup)
319   }
320 }