form_data.js (12249B)
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 // Public API 13 module.exports = FormData; 14 15 // make it a Stream 16 util.inherits(FormData, CombinedStream); 17 18 /** 19 * Create readable "multipart/form-data" streams. 20 * Can be used to submit forms 21 * and file uploads to other web applications. 22 * 23 * @constructor 24 * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream 25 */ 26 function FormData(options) { 27 if (!(this instanceof FormData)) { 28 return new FormData(); 29 } 30 31 this._overheadLength = 0; 32 this._valueLength = 0; 33 this._valuesToMeasure = []; 34 35 CombinedStream.call(this); 36 37 options = options || {}; 38 for (var option in options) { 39 this[option] = options[option]; 40 } 41 } 42 43 FormData.LINE_BREAK = '\r\n'; 44 FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; 45 46 FormData.prototype.append = function(field, value, options) { 47 48 options = options || {}; 49 50 // allow filename as single option 51 if (typeof options == 'string') { 52 options = {filename: options}; 53 } 54 55 var append = CombinedStream.prototype.append.bind(this); 56 57 // all that streamy business can't handle numbers 58 if (typeof value == 'number') { 59 value = '' + value; 60 } 61 62 // https://github.com/felixge/node-form-data/issues/38 63 if (util.isArray(value)) { 64 // Please convert your array into string 65 // the way web server expects it 66 this._error(new Error('Arrays are not supported.')); 67 return; 68 } 69 70 var header = this._multiPartHeader(field, value, options); 71 var footer = this._multiPartFooter(); 72 73 append(header); 74 append(value); 75 append(footer); 76 77 // pass along options.knownLength 78 this._trackLength(header, value, options); 79 }; 80 81 FormData.prototype._trackLength = function(header, value, options) { 82 var valueLength = 0; 83 84 // used w/ getLengthSync(), when length is known. 85 // e.g. for streaming directly from a remote server, 86 // w/ a known file a size, and not wanting to wait for 87 // incoming file to finish to get its size. 88 if (options.knownLength != null) { 89 valueLength += +options.knownLength; 90 } else if (Buffer.isBuffer(value)) { 91 valueLength = value.length; 92 } else if (typeof value === 'string') { 93 valueLength = Buffer.byteLength(value); 94 } 95 96 this._valueLength += valueLength; 97 98 // @check why add CRLF? does this account for custom/multiple CRLFs? 99 this._overheadLength += 100 Buffer.byteLength(header) + 101 FormData.LINE_BREAK.length; 102 103 // empty or either doesn't have path or not an http response 104 if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) { 105 return; 106 } 107 108 // no need to bother with the length 109 if (!options.knownLength) { 110 this._valuesToMeasure.push(value); 111 } 112 }; 113 114 FormData.prototype._lengthRetriever = function(value, callback) { 115 116 if (value.hasOwnProperty('fd')) { 117 118 // take read range into a account 119 // `end` = Infinity –> read file till the end 120 // 121 // TODO: Looks like there is bug in Node fs.createReadStream 122 // it doesn't respect `end` options without `start` options 123 // Fix it when node fixes it. 124 // https://github.com/joyent/node/issues/7819 125 if (value.end != undefined && value.end != Infinity && value.start != undefined) { 126 127 // when end specified 128 // no need to calculate range 129 // inclusive, starts with 0 130 callback(null, value.end + 1 - (value.start ? value.start : 0)); 131 132 // not that fast snoopy 133 } else { 134 // still need to fetch file size from fs 135 fs.stat(value.path, function(err, stat) { 136 137 var fileSize; 138 139 if (err) { 140 callback(err); 141 return; 142 } 143 144 // update final size based on the range options 145 fileSize = stat.size - (value.start ? value.start : 0); 146 callback(null, fileSize); 147 }); 148 } 149 150 // or http response 151 } else if (value.hasOwnProperty('httpVersion')) { 152 callback(null, +value.headers['content-length']); 153 154 // or request stream http://github.com/mikeal/request 155 } else if (value.hasOwnProperty('httpModule')) { 156 // wait till response come back 157 value.on('response', function(response) { 158 value.pause(); 159 callback(null, +response.headers['content-length']); 160 }); 161 value.resume(); 162 163 // something else 164 } else { 165 callback('Unknown stream'); 166 } 167 }; 168 169 FormData.prototype._multiPartHeader = function(field, value, options) { 170 // custom header specified (as string)? 171 // it becomes responsible for boundary 172 // (e.g. to handle extra CRLFs on .NET servers) 173 if (typeof options.header == 'string') { 174 return options.header; 175 } 176 177 var contentDisposition = this._getContentDisposition(value, options); 178 var contentType = this._getContentType(value, options); 179 180 var contents = ''; 181 var headers = { 182 // add custom disposition as third element or keep it two elements if not 183 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), 184 // if no content type. allow it to be empty array 185 'Content-Type': [].concat(contentType || []) 186 }; 187 188 // allow custom headers. 189 if (typeof options.header == 'object') { 190 populate(headers, options.header); 191 } 192 193 var header; 194 for (var prop in headers) { 195 if (!headers.hasOwnProperty(prop)) continue; 196 header = headers[prop]; 197 198 // skip nullish headers. 199 if (header == null) { 200 continue; 201 } 202 203 // convert all headers to arrays. 204 if (!Array.isArray(header)) { 205 header = [header]; 206 } 207 208 // add non-empty headers. 209 if (header.length) { 210 contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK; 211 } 212 } 213 214 return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; 215 }; 216 217 FormData.prototype._getContentDisposition = function(value, options) { 218 219 var filename 220 , contentDisposition 221 ; 222 223 if (typeof options.filepath === 'string') { 224 // custom filepath for relative paths 225 filename = path.normalize(options.filepath).replace(/\\/g, '/'); 226 } else if (options.filename || value.name || value.path) { 227 // custom filename take precedence 228 // formidable and the browser add a name property 229 // fs- and request- streams have path property 230 filename = path.basename(options.filename || value.name || value.path); 231 } else if (value.readable && value.hasOwnProperty('httpVersion')) { 232 // or try http response 233 filename = path.basename(value.client._httpMessage.path); 234 } 235 236 if (filename) { 237 contentDisposition = 'filename="' + filename + '"'; 238 } 239 240 return contentDisposition; 241 }; 242 243 FormData.prototype._getContentType = function(value, options) { 244 245 // use custom content-type above all 246 var contentType = options.contentType; 247 248 // or try `name` from formidable, browser 249 if (!contentType && value.name) { 250 contentType = mime.lookup(value.name); 251 } 252 253 // or try `path` from fs-, request- streams 254 if (!contentType && value.path) { 255 contentType = mime.lookup(value.path); 256 } 257 258 // or if it's http-reponse 259 if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { 260 contentType = value.headers['content-type']; 261 } 262 263 // or guess it from the filepath or filename 264 if (!contentType && (options.filepath || options.filename)) { 265 contentType = mime.lookup(options.filepath || options.filename); 266 } 267 268 // fallback to the default content type if `value` is not simple value 269 if (!contentType && typeof value == 'object') { 270 contentType = FormData.DEFAULT_CONTENT_TYPE; 271 } 272 273 return contentType; 274 }; 275 276 FormData.prototype._multiPartFooter = function() { 277 return function(next) { 278 var footer = FormData.LINE_BREAK; 279 280 var lastPart = (this._streams.length === 0); 281 if (lastPart) { 282 footer += this._lastBoundary(); 283 } 284 285 next(footer); 286 }.bind(this); 287 }; 288 289 FormData.prototype._lastBoundary = function() { 290 return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; 291 }; 292 293 FormData.prototype.getHeaders = function(userHeaders) { 294 var header; 295 var formHeaders = { 296 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() 297 }; 298 299 for (header in userHeaders) { 300 if (userHeaders.hasOwnProperty(header)) { 301 formHeaders[header.toLowerCase()] = userHeaders[header]; 302 } 303 } 304 305 return formHeaders; 306 }; 307 308 FormData.prototype.getBoundary = function() { 309 if (!this._boundary) { 310 this._generateBoundary(); 311 } 312 313 return this._boundary; 314 }; 315 316 FormData.prototype._generateBoundary = function() { 317 // This generates a 50 character boundary similar to those used by Firefox. 318 // They are optimized for boyer-moore parsing. 319 var boundary = '--------------------------'; 320 for (var i = 0; i < 24; i++) { 321 boundary += Math.floor(Math.random() * 10).toString(16); 322 } 323 324 this._boundary = boundary; 325 }; 326 327 // Note: getLengthSync DOESN'T calculate streams length 328 // As workaround one can calculate file size manually 329 // and add it as knownLength option 330 FormData.prototype.getLengthSync = function() { 331 var knownLength = this._overheadLength + this._valueLength; 332 333 // Don't get confused, there are 3 "internal" streams for each keyval pair 334 // so it basically checks if there is any value added to the form 335 if (this._streams.length) { 336 knownLength += this._lastBoundary().length; 337 } 338 339 // https://github.com/form-data/form-data/issues/40 340 if (!this.hasKnownLength()) { 341 // Some async length retrievers are present 342 // therefore synchronous length calculation is false. 343 // Please use getLength(callback) to get proper length 344 this._error(new Error('Cannot calculate proper length in synchronous way.')); 345 } 346 347 return knownLength; 348 }; 349 350 // Public API to check if length of added values is known 351 // https://github.com/form-data/form-data/issues/196 352 // https://github.com/form-data/form-data/issues/262 353 FormData.prototype.hasKnownLength = function() { 354 var hasKnownLength = true; 355 356 if (this._valuesToMeasure.length) { 357 hasKnownLength = false; 358 } 359 360 return hasKnownLength; 361 }; 362 363 FormData.prototype.getLength = function(cb) { 364 var knownLength = this._overheadLength + this._valueLength; 365 366 if (this._streams.length) { 367 knownLength += this._lastBoundary().length; 368 } 369 370 if (!this._valuesToMeasure.length) { 371 process.nextTick(cb.bind(this, null, knownLength)); 372 return; 373 } 374 375 asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { 376 if (err) { 377 cb(err); 378 return; 379 } 380 381 values.forEach(function(length) { 382 knownLength += length; 383 }); 384 385 cb(null, knownLength); 386 }); 387 }; 388 389 FormData.prototype.submit = function(params, cb) { 390 var request 391 , options 392 , defaults = {method: 'post'} 393 ; 394 395 // parse provided url if it's string 396 // or treat it as options object 397 if (typeof params == 'string') { 398 399 params = parseUrl(params); 400 options = populate({ 401 port: params.port, 402 path: params.pathname, 403 host: params.hostname, 404 protocol: params.protocol 405 }, defaults); 406 407 // use custom params 408 } else { 409 410 options = populate(params, defaults); 411 // if no port provided use default one 412 if (!options.port) { 413 options.port = options.protocol == 'https:' ? 443 : 80; 414 } 415 } 416 417 // put that good code in getHeaders to some use 418 options.headers = this.getHeaders(params.headers); 419 420 // https if specified, fallback to http in any other case 421 if (options.protocol == 'https:') { 422 request = https.request(options); 423 } else { 424 request = http.request(options); 425 } 426 427 // get content length and fire away 428 this.getLength(function(err, length) { 429 if (err) { 430 this._error(err); 431 return; 432 } 433 434 // add content length 435 request.setHeader('Content-Length', length); 436 437 this.pipe(request); 438 if (cb) { 439 request.on('error', cb); 440 request.on('response', cb.bind(this, null)); 441 } 442 }.bind(this)); 443 444 return request; 445 }; 446 447 FormData.prototype._error = function(err) { 448 if (!this.error) { 449 this.error = err; 450 this.pause(); 451 this.emit('error', err); 452 } 453 }; 454 455 FormData.prototype.toString = function () { 456 return '[object FormData]'; 457 };