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