UNPKG

6.8 kBJavaScriptView Raw
1
2var fs = require("fs")
3, os = require("os")
4, path = require("path")
5, qs = require("querystring")
6, Writable = require("stream").Writable
7, accept = require("./accept.js")
8, rnrn = Buffer.from("\r\n\r\n")
9, negotiateContent = accept({
10 'application/json': function(str) {
11 return JSON.parse(str || "{}")
12 },
13 'application/x-www-form-urlencoded': function(str) {
14 return qs.parse(str)
15 },
16 // Subtypes:
17 // - alternative
18 // - byterange https://tools.ietf.org/html/rfc7233#section-5.4.1
19 // - digest
20 // - encrypted
21 // - form-data https://tools.ietf.org/html/rfc7578
22 // - mixed
23 // - related https://tools.ietf.org/html/rfc2387
24 // - report
25 // - signed
26 // - x-mixed-replace
27 'multipart/*;boundary=': null
28})
29, negotiateDisposition = accept([
30 'form-data;name="";filename=""'
31])
32, seq = 0
33
34
35module.exports = getContent
36getContent.multipart = multipart
37
38
39makeTable(rnrn)
40
41function getContent(next, reqOpts) {
42 var i, tmp
43 , req = this
44 , head = req.headers
45 , negod = negotiateContent(head["content-type"] || head.accept || "*")
46 , stream = req
47
48 req.body = {}
49
50 if (head["content-encoding"]) {
51 tmp = head["content-encoding"].split(/\W+/)
52 for (i = tmp.length; i--; ) {
53 if (tmp[i] === "gzip" || tmp[i] === "deflate") {
54 // Decompress Gzip or Deflate by auto-detecting the header
55 stream = stream.pipe(zlib.createUnzip())
56 } else if (tmp[i] && tmp[i] !== "identity") {
57 throw "Unsupported Media Type"
58 }
59 }
60 }
61
62 if (negod.type === "multipart") {
63 stream = stream.pipe(multipart(negod.boundary, reqOpts || {}, req))
64
65 stream.on("field", function(negod) {
66 req.body[negod.name] = negod.content.toString()
67 })
68 stream.on("file", function(negod) {
69 if (!req.files) req.files = []
70 req.files.push(negod)
71 })
72 stream.on("finish", function() {
73 next(null, req.body, req.files)
74 if (req.files) {
75 for (var i = req.files.length; i--; ) {
76 if (req.files[i].tmp) fs.unlink(req.files[i].tmp, nop)
77 }
78 }
79 })
80 } else {
81 tmp = ""
82 stream.on("data", function handleData(data) {
83 tmp += data
84 // FLOOD ATTACK OR FAULTY CLIENT, NUKE REQUEST
85 if (tmp.length > req.opts.maxBodySize) {
86 stream.destroy("Payload Too Large") // 431 Payload Too Large
87 }
88 })
89 .on("end", handleEnd)
90 }
91
92 stream.on("error", next)
93
94 function handleEnd() {
95 try {
96 req.body = typeof negod.o === "function" ? negod.o(tmp) : tmp
97 next(null, req.body, req.files, negod)
98 } catch (e) {
99 next(e)
100 }
101 }
102}
103
104function multipart(boundary, reqOpts, req) {
105 // nodejs HTTP_MAX_HEADER_SIZE (80*1024)
106 // { "name": "in[]", filename: "a.jpg", tmp: "/tmp/a.123", size: 123 }
107 if (typeof boundary === "string") {
108 boundary = Buffer.from("\r\n--" + boundary)
109 }
110
111 seq++
112 makeTable(boundary)
113
114 var headers, fileStream
115 , negod = reqOpts.preamble && { preamble: true }
116 , needle = boundary
117 , bufs = [rnrn.slice(2)]
118 , bufsBytes = 2
119 , nextPos = needle.length - 3
120 , remainingFields = number(reqOpts.maxFields, req.opts.maxFields, 1000)
121 , remainingFiles = number(reqOpts.maxFiles, req.opts.maxFiles, 1000)
122
123 return new Writable({
124 write: function(chunk, enc, cb) {
125 var buf, bufNum, i, j
126 , writable = this
127 , pos = nextPos
128 , len = chunk.length
129 , last = needle[needle.length - 1]
130 , cut = 0
131
132 if (pos > len) {
133 bufs.push(chunk)
134 bufsBytes += len
135 nextPos -= len
136 return cb()
137 }
138
139 jump:for (; pos < len; ) {
140 if (chunk[pos] === last) {
141 buf = chunk
142 bufNum = bufs.length
143 i = needle.length - 1
144 j = pos
145 for (; i > 0; ) {
146 if (j < 1 && bufNum > 0) {
147 buf = bufs[--bufNum]
148 j = buf.length
149 }
150 if (needle[--i] !== buf[--j]) {
151 pos += needle.jump[last]
152 continue jump
153 }
154 }
155 // match found
156 if (bufsBytes > 0) {
157 bufs.push(chunk)
158 buf = Buffer.concat(bufs, pos + bufsBytes - needle.length + 1)
159 bufsBytes = bufs.length = 0
160 } else if (cut > 0 || pos < len - 1) {
161 buf = buf.slice(cut, pos - needle.length + 1)
162 }
163 if (needle === boundary) {
164 if (negod) {
165 if (!remainingFields--) return writable.destroy({ code: 413, message: "maxFields exceeded"})
166 negod.content = buf
167 writable.emit(negod.filename === "" ? "field" : "file", negod)
168 negod = null
169 } else if (fileStream) {
170 fileStream.end(buf)
171 fileStream = null
172 }
173 needle = rnrn
174 } else {
175 // content start
176 headers = parseHeaders(buf.toString())
177 negod = negotiateDisposition(headers["content-disposition"])
178 negod.headers = headers
179
180 if (negod.filename && reqOpts.path !== null) {
181 if (!remainingFiles--) return writable.destroy({ code: 413, message: "maxFiles exceeded"})
182 writable.emit("file", negod, saveTo)
183 if (!fileStream) {
184 saveTo(
185 (reqOpts.path || os.tmpdir() + "/upload-") +
186 process.pid + "-" + seq + "-" + remainingFiles
187 )
188 }
189 }
190 needle = boundary
191 }
192 cut = pos + 1
193 last = needle[needle.length - 1]
194 pos += needle.length
195 } else {
196 pos += needle.jump[chunk[pos]]
197 }
198 }
199
200 nextPos = pos - len
201
202 if (cut < len) {
203 bufs.push(cut ? chunk.slice(cut) : chunk)
204 bufsBytes += bufs[bufs.length - 1].length
205 }
206
207 if (fileStream) {
208 for (; bufs.length > 1 && bufsBytes - bufs[bufs.length - 1].length > needle.length; ) {
209 if (!fileStream.write(bufs.pop())) {
210 return fileStream.once("drain", cb)
211 }
212 }
213 }
214
215 cb()
216 }
217 })
218
219 function saveTo(stream) {
220 fileStream = (
221 typeof stream === "string" ?
222 fs.createWriteStream(negod.tmp = stream) :
223 stream
224 )
225 negod = null
226 }
227}
228
229function nop() {}
230
231function number(a, b, c) {
232 return (
233 typeof a === "number" ? a :
234 typeof b === "number" ? b :
235 c
236 )
237}
238
239// multipart/form-data part accepts only Content-Type, Content-Disposition, and (in limited circumstances) Content-Transfer-Encoding.
240// Other header fields MUST NOT be included and MUST be ignored.
241
242// Content-Transfer-Encoding: 7bit / 8bit / binary / quoted-printable / base64 / ietf-token / x-token
243//
244// User Agents that recognize Multipart/Related will ignore the Content-Disposition header's disposition type.
245// Other User Agents will process the Multipart/Related as Multipart/Mixed and may make use of that header's information.
246
247function parseHeaders(str) {
248 // 4.8. Other "Content-" Header Fields
249 var i
250 , headers = {}
251 , lines = str.split("\r\n")
252 , len = lines.length
253 for (; len; ) {
254 i = lines[--len].indexOf(":")
255 if (i > 0) {
256 headers[
257 lines[len].slice(0, i).toLowerCase()
258 ] = lines[len].slice(i + 1).trim()
259 }
260 }
261 return headers
262}
263
264function makeTable(buf) {
265 var len = buf.length
266 , i = 0
267 , pos = len - 1
268 , jump = buf.jump = new Uint8Array(256).fill(len)
269
270 for (; i < pos; ++i) {
271 jump[buf[i]] = pos - i
272 }
273}
274
275