UNPKG

8.66 kBJavaScriptView Raw
1var CombinedStream = require('combined-stream');
2var util = require('util');
3var path = require('path');
4var http = require('http');
5var https = require('https');
6var parseUrl = require('url').parse;
7var fs = require('fs');
8var mime = require('mime');
9var async = require('async');
10
11module.exports = FormData;
12function FormData() {
13 this._overheadLength = 0;
14 this._valueLength = 0;
15 this._lengthRetrievers = [];
16
17 CombinedStream.call(this);
18}
19util.inherits(FormData, CombinedStream);
20
21FormData.LINE_BREAK = '\r\n';
22
23FormData.prototype.append = function(field, value, options) {
24 options = options || {};
25
26 var append = CombinedStream.prototype.append.bind(this);
27
28 // all that streamy business can't handle numbers
29 if (typeof value == 'number') value = ''+value;
30
31 // https://github.com/felixge/node-form-data/issues/38
32 if (util.isArray(value)) {
33 // Please convert your array into string
34 // the way web server expects it
35 this._error(new Error('Arrays are not supported.'));
36 return;
37 }
38
39 var header = this._multiPartHeader(field, value, options);
40 var footer = this._multiPartFooter(field, value, options);
41
42 append(header);
43 append(value);
44 append(footer);
45
46 // pass along options.knownLength
47 this._trackLength(header, value, options);
48};
49
50FormData.prototype._trackLength = function(header, value, options) {
51 var valueLength = 0;
52
53 // used w/ getLengthSync(), when length is known.
54 // e.g. for streaming directly from a remote server,
55 // w/ a known file a size, and not wanting to wait for
56 // incoming file to finish to get its size.
57 if (options.knownLength != null) {
58 valueLength += +options.knownLength;
59 } else if (Buffer.isBuffer(value)) {
60 valueLength = value.length;
61 } else if (typeof value === 'string') {
62 valueLength = Buffer.byteLength(value);
63 }
64
65 this._valueLength += valueLength;
66
67 // @check why add CRLF? does this account for custom/multiple CRLFs?
68 this._overheadLength +=
69 Buffer.byteLength(header) +
70 + FormData.LINE_BREAK.length;
71
72 // empty or either doesn't have path or not an http response
73 if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
74 return;
75 }
76
77 // no need to bother with the length
78 if (!options.knownLength)
79 this._lengthRetrievers.push(function(next) {
80
81 if (value.hasOwnProperty('fd')) {
82 fs.stat(value.path, function(err, stat) {
83 if (err) {
84 next(err);
85 return;
86 }
87
88 next(null, stat.size);
89 });
90
91 // or http response
92 } else if (value.hasOwnProperty('httpVersion')) {
93 next(null, +value.headers['content-length']);
94
95 // or request stream http://github.com/mikeal/request
96 } else if (value.hasOwnProperty('httpModule')) {
97 // wait till response come back
98 value.on('response', function(response) {
99 value.pause();
100 next(null, +response.headers['content-length']);
101 });
102 value.resume();
103
104 // something else
105 } else {
106 next('Unknown stream');
107 }
108 });
109};
110
111FormData.prototype._multiPartHeader = function(field, value, options) {
112 var boundary = this.getBoundary();
113 var header = '';
114
115 // custom header specified (as string)?
116 // it becomes responsible for boundary
117 // (e.g. to handle extra CRLFs on .NET servers)
118 if (options.header != null) {
119 header = options.header;
120 } else {
121 header += '--' + boundary + FormData.LINE_BREAK +
122 'Content-Disposition: form-data; name="' + field + '"';
123
124 // fs- and request- streams have path property
125 // or use custom filename and/or contentType
126 // TODO: Use request's response mime-type
127 if (options.filename || value.path) {
128 header +=
129 '; filename="' + path.basename(options.filename || value.path) + '"' + FormData.LINE_BREAK +
130 'Content-Type: ' + (options.contentType || mime.lookup(options.filename || value.path));
131
132 // http response has not
133 } else if (value.readable && value.hasOwnProperty('httpVersion')) {
134 header +=
135 '; filename="' + path.basename(value.client._httpMessage.path) + '"' + FormData.LINE_BREAK +
136 'Content-Type: ' + value.headers['content-type'];
137 }
138
139 header += FormData.LINE_BREAK + FormData.LINE_BREAK;
140 }
141
142 return header;
143};
144
145FormData.prototype._multiPartFooter = function(field, value, options) {
146 return function(next) {
147 var footer = FormData.LINE_BREAK;
148
149 var lastPart = (this._streams.length === 0);
150 if (lastPart) {
151 footer += this._lastBoundary();
152 }
153
154 next(footer);
155 }.bind(this);
156};
157
158FormData.prototype._lastBoundary = function() {
159 return '--' + this.getBoundary() + '--';
160};
161
162FormData.prototype.getHeaders = function(userHeaders) {
163 var formHeaders = {
164 'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
165 };
166
167 for (var header in userHeaders) {
168 formHeaders[header.toLowerCase()] = userHeaders[header];
169 }
170
171 return formHeaders;
172}
173
174FormData.prototype.getCustomHeaders = function(contentType) {
175 contentType = contentType ? contentType : 'multipart/form-data';
176
177 var formHeaders = {
178 'content-type': contentType + '; boundary=' + this.getBoundary(),
179 'content-length': this.getLengthSync()
180 };
181
182 return formHeaders;
183}
184
185FormData.prototype.getBoundary = function() {
186 if (!this._boundary) {
187 this._generateBoundary();
188 }
189
190 return this._boundary;
191};
192
193FormData.prototype._generateBoundary = function() {
194 // This generates a 50 character boundary similar to those used by Firefox.
195 // They are optimized for boyer-moore parsing.
196 var boundary = '--------------------------';
197 for (var i = 0; i < 24; i++) {
198 boundary += Math.floor(Math.random() * 10).toString(16);
199 }
200
201 this._boundary = boundary;
202};
203
204// Note: getLengthSync DOESN'T calculate streams length
205// As workaround one can calculate file size manually
206// and add it as knownLength option
207FormData.prototype.getLengthSync = function(debug) {
208 var knownLength = this._overheadLength + this._valueLength;
209
210 // Don't get confused, there are 3 "internal" streams for each keyval pair
211 // so it basically checks if there is any value added to the form
212 if (this._streams.length) {
213 knownLength += this._lastBoundary().length;
214 }
215
216 // https://github.com/felixge/node-form-data/issues/40
217 if (this._lengthRetrievers.length) {
218 // Some async length retrivers are present
219 // therefore synchronous length calculation is false.
220 // Please use getLength(callback) to get proper length
221 this._error(new Error('Cannot calculate proper length in synchronous way.'));
222 }
223
224 return knownLength;
225};
226
227FormData.prototype.getLength = function(cb) {
228 var knownLength = this._overheadLength + this._valueLength;
229
230 if (this._streams.length) {
231 knownLength += this._lastBoundary().length;
232 }
233
234 if (!this._lengthRetrievers.length) {
235 process.nextTick(cb.bind(this, null, knownLength));
236 return;
237 }
238
239 async.parallel(this._lengthRetrievers, function(err, values) {
240 if (err) {
241 cb(err);
242 return;
243 }
244
245 values.forEach(function(length) {
246 knownLength += length;
247 });
248
249 cb(null, knownLength);
250 });
251};
252
253FormData.prototype.submit = function(params, cb) {
254
255 var request
256 , options
257 , defaults = {
258 method : 'post',
259 headers: this.getHeaders()
260 };
261
262 // parse provided url if it's string
263 // or treat it as options object
264 if (typeof params == 'string') {
265 params = parseUrl(params);
266
267 options = populate({
268 port: params.port,
269 path: params.pathname,
270 host: params.hostname
271 }, defaults);
272 }
273 else // use custom params
274 {
275 options = populate(params, defaults);
276 // if no port provided use default one
277 if (!options.port) {
278 options.port = options.protocol == 'https:' ? 443 : 80;
279 }
280 }
281
282 // https if specified, fallback to http in any other case
283 if (params.protocol == 'https:') {
284 request = https.request(options);
285 } else {
286 request = http.request(options);
287 }
288
289 // get content length and fire away
290 this.getLength(function(err, length) {
291
292 // TODO: Add chunked encoding when no length (if err)
293
294 // add content length
295 request.setHeader('Content-Length', length);
296
297 this.pipe(request);
298 if (cb) {
299 request.on('error', cb);
300 request.on('response', cb.bind(this, null));
301 }
302 }.bind(this));
303
304 return request;
305};
306
307FormData.prototype._error = function(err) {
308 if (this.error) return;
309
310 this.error = err;
311 this.pause();
312 this.emit('error', err);
313};
314
315/*
316 * Santa's little helpers
317 */
318
319// populates missing values
320function populate(dst, src) {
321 for (var prop in src) {
322 if (!dst[prop]) dst[prop] = src[prop];
323 }
324 return dst;
325}