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');
|
9 | var async = require('async');
|
10 |
|
11 | module.exports = FormData;
|
12 | function FormData() {
|
13 | this._overheadLength = 0;
|
14 | this._valueLength = 0;
|
15 | this._lengthRetrievers = [];
|
16 |
|
17 | CombinedStream.call(this);
|
18 | }
|
19 | util.inherits(FormData, CombinedStream);
|
20 |
|
21 | FormData.LINE_BREAK = '\r\n';
|
22 |
|
23 | FormData.prototype.append = function(field, value, options) {
|
24 | options = options || {};
|
25 |
|
26 | var append = CombinedStream.prototype.append.bind(this);
|
27 |
|
28 |
|
29 | if (typeof value == 'number') value = ''+value;
|
30 |
|
31 |
|
32 | if (util.isArray(value)) {
|
33 |
|
34 |
|
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 |
|
47 | this._trackLength(header, value, options);
|
48 | };
|
49 |
|
50 | FormData.prototype._trackLength = function(header, value, options) {
|
51 | var valueLength = 0;
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
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 |
|
68 | this._overheadLength +=
|
69 | Buffer.byteLength(header) +
|
70 | + FormData.LINE_BREAK.length;
|
71 |
|
72 |
|
73 | if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
|
74 | return;
|
75 | }
|
76 |
|
77 |
|
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 |
|
92 | } else if (value.hasOwnProperty('httpVersion')) {
|
93 | next(null, +value.headers['content-length']);
|
94 |
|
95 |
|
96 | } else if (value.hasOwnProperty('httpModule')) {
|
97 |
|
98 | value.on('response', function(response) {
|
99 | value.pause();
|
100 | next(null, +response.headers['content-length']);
|
101 | });
|
102 | value.resume();
|
103 |
|
104 |
|
105 | } else {
|
106 | next('Unknown stream');
|
107 | }
|
108 | });
|
109 | };
|
110 |
|
111 | FormData.prototype._multiPartHeader = function(field, value, options) {
|
112 | var boundary = this.getBoundary();
|
113 | var header = '';
|
114 |
|
115 |
|
116 |
|
117 |
|
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 |
|
125 |
|
126 |
|
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 |
|
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 |
|
145 | FormData.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 |
|
158 | FormData.prototype._lastBoundary = function() {
|
159 | return '--' + this.getBoundary() + '--';
|
160 | };
|
161 |
|
162 | FormData.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 |
|
174 | FormData.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 |
|
185 | FormData.prototype.getBoundary = function() {
|
186 | if (!this._boundary) {
|
187 | this._generateBoundary();
|
188 | }
|
189 |
|
190 | return this._boundary;
|
191 | };
|
192 |
|
193 | FormData.prototype._generateBoundary = function() {
|
194 |
|
195 |
|
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 |
|
205 |
|
206 |
|
207 | FormData.prototype.getLengthSync = function(debug) {
|
208 | var knownLength = this._overheadLength + this._valueLength;
|
209 |
|
210 |
|
211 |
|
212 | if (this._streams.length) {
|
213 | knownLength += this._lastBoundary().length;
|
214 | }
|
215 |
|
216 |
|
217 | if (this._lengthRetrievers.length) {
|
218 |
|
219 |
|
220 |
|
221 | this._error(new Error('Cannot calculate proper length in synchronous way.'));
|
222 | }
|
223 |
|
224 | return knownLength;
|
225 | };
|
226 |
|
227 | FormData.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 |
|
253 | FormData.prototype.submit = function(params, cb) {
|
254 |
|
255 | var request
|
256 | , options
|
257 | , defaults = {
|
258 | method : 'post',
|
259 | headers: this.getHeaders()
|
260 | };
|
261 |
|
262 |
|
263 |
|
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
|
274 | {
|
275 | options = populate(params, defaults);
|
276 |
|
277 | if (!options.port) {
|
278 | options.port = options.protocol == 'https:' ? 443 : 80;
|
279 | }
|
280 | }
|
281 |
|
282 |
|
283 | if (params.protocol == 'https:') {
|
284 | request = https.request(options);
|
285 | } else {
|
286 | request = http.request(options);
|
287 | }
|
288 |
|
289 |
|
290 | this.getLength(function(err, length) {
|
291 |
|
292 |
|
293 |
|
294 |
|
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 |
|
307 | FormData.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 |
|
317 |
|
318 |
|
319 |
|
320 | function populate(dst, src) {
|
321 | for (var prop in src) {
|
322 | if (!dst[prop]) dst[prop] = src[prop];
|
323 | }
|
324 | return dst;
|
325 | }
|