1 | var CombinedStream = require('combined-stream');
|
2 | var util = require('util');
|
3 | var path = require('path');
|
4 | var http = require('http');
|
5 | var https = require('https');
|
6 | var parseUrl = require('url').parse;
|
7 | var fs = require('fs');
|
8 | var mime = require('mime-types');
|
9 | var asynckit = require('asynckit');
|
10 | var populate = require('./populate.js');
|
11 |
|
12 |
|
13 | module.exports = FormData;
|
14 |
|
15 |
|
16 | util.inherits(FormData, CombinedStream);
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | function FormData() {
|
26 | if (!(this instanceof FormData)) {
|
27 | return new FormData();
|
28 | }
|
29 |
|
30 | this._overheadLength = 0;
|
31 | this._valueLength = 0;
|
32 | this._valuesToMeasure = [];
|
33 |
|
34 | CombinedStream.call(this);
|
35 | }
|
36 |
|
37 | FormData.LINE_BREAK = '\r\n';
|
38 | FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
39 |
|
40 | FormData.prototype.append = function(field, value, options) {
|
41 |
|
42 | options = options || {};
|
43 |
|
44 |
|
45 | if (typeof options == 'string') {
|
46 | options = {filename: options};
|
47 | }
|
48 |
|
49 | var append = CombinedStream.prototype.append.bind(this);
|
50 |
|
51 |
|
52 | if (typeof value == 'number') {
|
53 | value = '' + value;
|
54 | }
|
55 |
|
56 |
|
57 | if (util.isArray(value)) {
|
58 |
|
59 |
|
60 | this._error(new Error('Arrays are not supported.'));
|
61 | return;
|
62 | }
|
63 |
|
64 | var header = this._multiPartHeader(field, value, options);
|
65 | var footer = this._multiPartFooter();
|
66 |
|
67 | append(header);
|
68 | append(value);
|
69 | append(footer);
|
70 |
|
71 |
|
72 | this._trackLength(header, value, options);
|
73 | };
|
74 |
|
75 | FormData.prototype._trackLength = function(header, value, options) {
|
76 | var valueLength = 0;
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | if (options.knownLength != null) {
|
83 | valueLength += +options.knownLength;
|
84 | } else if (Buffer.isBuffer(value)) {
|
85 | valueLength = value.length;
|
86 | } else if (typeof value === 'string') {
|
87 | valueLength = Buffer.byteLength(value);
|
88 | }
|
89 |
|
90 | this._valueLength += valueLength;
|
91 |
|
92 |
|
93 | this._overheadLength +=
|
94 | Buffer.byteLength(header) +
|
95 | FormData.LINE_BREAK.length;
|
96 |
|
97 |
|
98 | if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
|
99 | return;
|
100 | }
|
101 |
|
102 |
|
103 | if (!options.knownLength) {
|
104 | this._valuesToMeasure.push(value);
|
105 | }
|
106 | };
|
107 |
|
108 | FormData.prototype._lengthRetriever = function(value, callback) {
|
109 |
|
110 | if (value.hasOwnProperty('fd')) {
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | if (value.end != undefined && value.end != Infinity && value.start != undefined) {
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | callback(null, value.end + 1 - (value.start ? value.start : 0));
|
125 |
|
126 |
|
127 | } else {
|
128 |
|
129 | fs.stat(value.path, function(err, stat) {
|
130 |
|
131 | var fileSize;
|
132 |
|
133 | if (err) {
|
134 | callback(err);
|
135 | return;
|
136 | }
|
137 |
|
138 |
|
139 | fileSize = stat.size - (value.start ? value.start : 0);
|
140 | callback(null, fileSize);
|
141 | });
|
142 | }
|
143 |
|
144 |
|
145 | } else if (value.hasOwnProperty('httpVersion')) {
|
146 | callback(null, +value.headers['content-length']);
|
147 |
|
148 |
|
149 | } else if (value.hasOwnProperty('httpModule')) {
|
150 |
|
151 | value.on('response', function(response) {
|
152 | value.pause();
|
153 | callback(null, +response.headers['content-length']);
|
154 | });
|
155 | value.resume();
|
156 |
|
157 |
|
158 | } else {
|
159 | callback('Unknown stream');
|
160 | }
|
161 | };
|
162 |
|
163 | FormData.prototype._multiPartHeader = function(field, value, options) {
|
164 |
|
165 |
|
166 |
|
167 | if (typeof options.header == 'string') {
|
168 | return options.header;
|
169 | }
|
170 |
|
171 | var contentDisposition = this._getContentDisposition(value, options);
|
172 | var contentType = this._getContentType(value, options);
|
173 |
|
174 | var contents = '';
|
175 | var headers = {
|
176 |
|
177 | 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
|
178 |
|
179 | 'Content-Type': [].concat(contentType || [])
|
180 | };
|
181 |
|
182 |
|
183 | if (typeof options.header == 'object') {
|
184 | populate(headers, options.header);
|
185 | }
|
186 |
|
187 | var header;
|
188 | for (var prop in headers) {
|
189 | header = headers[prop];
|
190 |
|
191 |
|
192 | if (header == null) {
|
193 | continue;
|
194 | }
|
195 |
|
196 |
|
197 | if (!Array.isArray(header)) {
|
198 | header = [header];
|
199 | }
|
200 |
|
201 |
|
202 | if (header.length) {
|
203 | contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;
|
204 | }
|
205 | }
|
206 |
|
207 | return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
|
208 | };
|
209 |
|
210 | FormData.prototype._getContentDisposition = function(value, options) {
|
211 |
|
212 | var contentDisposition;
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | var filename = options.filename || value.name || value.path;
|
218 |
|
219 |
|
220 | if (!filename && value.readable && value.hasOwnProperty('httpVersion')) {
|
221 | filename = value.client._httpMessage.path;
|
222 | }
|
223 |
|
224 | if (filename) {
|
225 | contentDisposition = 'filename="' + path.basename(filename) + '"';
|
226 | }
|
227 |
|
228 | return contentDisposition;
|
229 | };
|
230 |
|
231 | FormData.prototype._getContentType = function(value, options) {
|
232 |
|
233 |
|
234 | var contentType = options.contentType;
|
235 |
|
236 |
|
237 | if (!contentType && value.name) {
|
238 | contentType = mime.lookup(value.name);
|
239 | }
|
240 |
|
241 |
|
242 | if (!contentType && value.path) {
|
243 | contentType = mime.lookup(value.path);
|
244 | }
|
245 |
|
246 |
|
247 | if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) {
|
248 | contentType = value.headers['content-type'];
|
249 | }
|
250 |
|
251 |
|
252 | if (!contentType && options.filename) {
|
253 | contentType = mime.lookup(options.filename);
|
254 | }
|
255 |
|
256 |
|
257 | if (!contentType && typeof value == 'object') {
|
258 | contentType = FormData.DEFAULT_CONTENT_TYPE;
|
259 | }
|
260 |
|
261 | return contentType;
|
262 | };
|
263 |
|
264 | FormData.prototype._multiPartFooter = function() {
|
265 | return function(next) {
|
266 | var footer = FormData.LINE_BREAK;
|
267 |
|
268 | var lastPart = (this._streams.length === 0);
|
269 | if (lastPart) {
|
270 | footer += this._lastBoundary();
|
271 | }
|
272 |
|
273 | next(footer);
|
274 | }.bind(this);
|
275 | };
|
276 |
|
277 | FormData.prototype._lastBoundary = function() {
|
278 | return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;
|
279 | };
|
280 |
|
281 | FormData.prototype.getHeaders = function(userHeaders) {
|
282 | var header;
|
283 | var formHeaders = {
|
284 | 'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
|
285 | };
|
286 |
|
287 | for (header in userHeaders) {
|
288 | if (userHeaders.hasOwnProperty(header)) {
|
289 | formHeaders[header.toLowerCase()] = userHeaders[header];
|
290 | }
|
291 | }
|
292 |
|
293 | return formHeaders;
|
294 | };
|
295 |
|
296 | FormData.prototype.getBoundary = function() {
|
297 | if (!this._boundary) {
|
298 | this._generateBoundary();
|
299 | }
|
300 |
|
301 | return this._boundary;
|
302 | };
|
303 |
|
304 | FormData.prototype._generateBoundary = function() {
|
305 |
|
306 |
|
307 | var boundary = '--------------------------';
|
308 | for (var i = 0; i < 24; i++) {
|
309 | boundary += Math.floor(Math.random() * 10).toString(16);
|
310 | }
|
311 |
|
312 | this._boundary = boundary;
|
313 | };
|
314 |
|
315 |
|
316 |
|
317 |
|
318 | FormData.prototype.getLengthSync = function() {
|
319 | var knownLength = this._overheadLength + this._valueLength;
|
320 |
|
321 |
|
322 |
|
323 | if (this._streams.length) {
|
324 | knownLength += this._lastBoundary().length;
|
325 | }
|
326 |
|
327 |
|
328 | if (!this.hasKnownLength()) {
|
329 |
|
330 |
|
331 |
|
332 | this._error(new Error('Cannot calculate proper length in synchronous way.'));
|
333 | }
|
334 |
|
335 | return knownLength;
|
336 | };
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | FormData.prototype.hasKnownLength = function() {
|
342 | var hasKnownLength = true;
|
343 |
|
344 | if (this._valuesToMeasure.length) {
|
345 | hasKnownLength = false;
|
346 | }
|
347 |
|
348 | return hasKnownLength;
|
349 | };
|
350 |
|
351 | FormData.prototype.getLength = function(cb) {
|
352 | var knownLength = this._overheadLength + this._valueLength;
|
353 |
|
354 | if (this._streams.length) {
|
355 | knownLength += this._lastBoundary().length;
|
356 | }
|
357 |
|
358 | if (!this._valuesToMeasure.length) {
|
359 | process.nextTick(cb.bind(this, null, knownLength));
|
360 | return;
|
361 | }
|
362 |
|
363 | asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {
|
364 | if (err) {
|
365 | cb(err);
|
366 | return;
|
367 | }
|
368 |
|
369 | values.forEach(function(length) {
|
370 | knownLength += length;
|
371 | });
|
372 |
|
373 | cb(null, knownLength);
|
374 | });
|
375 | };
|
376 |
|
377 | FormData.prototype.submit = function(params, cb) {
|
378 | var request
|
379 | , options
|
380 | , defaults = {method: 'post'}
|
381 | ;
|
382 |
|
383 |
|
384 |
|
385 | if (typeof params == 'string') {
|
386 |
|
387 | params = parseUrl(params);
|
388 | options = populate({
|
389 | port: params.port,
|
390 | path: params.pathname,
|
391 | host: params.hostname
|
392 | }, defaults);
|
393 |
|
394 |
|
395 | } else {
|
396 |
|
397 | options = populate(params, defaults);
|
398 |
|
399 | if (!options.port) {
|
400 | options.port = options.protocol == 'https:' ? 443 : 80;
|
401 | }
|
402 | }
|
403 |
|
404 |
|
405 | options.headers = this.getHeaders(params.headers);
|
406 |
|
407 |
|
408 | if (options.protocol == 'https:') {
|
409 | request = https.request(options);
|
410 | } else {
|
411 | request = http.request(options);
|
412 | }
|
413 |
|
414 |
|
415 | this.getLength(function(err, length) {
|
416 | if (err) {
|
417 | this._error(err);
|
418 | return;
|
419 | }
|
420 |
|
421 |
|
422 | request.setHeader('Content-Length', length);
|
423 |
|
424 | this.pipe(request);
|
425 | if (cb) {
|
426 | request.on('error', cb);
|
427 | request.on('response', cb.bind(this, null));
|
428 | }
|
429 | }.bind(this));
|
430 |
|
431 | return request;
|
432 | };
|
433 |
|
434 | FormData.prototype._error = function(err) {
|
435 | if (!this.error) {
|
436 | this.error = err;
|
437 | this.pause();
|
438 | this.emit('error', err);
|
439 | }
|
440 | };
|