codekingpro/portable-devtools
114k
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 Stream = require('stream').Stream;9var mime = require('mime-types');10var asynckit = require('asynckit');11var populate = require('./populate.js');12 13// Public API14module.exports = FormData;15 16// make it a Stream17util.inherits(FormData, CombinedStream);18 19/**20 * Create readable "multipart/form-data" streams.21 * Can be used to submit forms22 * and file uploads to other web applications.23 *24 * @constructor25 * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream26 */27function 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 44FormData.LINE_BREAK = '\r\n';45FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';46 47FormData.prototype.append = function(field, value, options) {48 49 options = options || {};50 51 // allow filename as single option52 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 numbers59 if (typeof value == 'number') {60 value = '' + value;61 }62 63 // https://github.com/felixge/node-form-data/issues/3864 if (util.isArray(value)) {65 // Please convert your array into string66 // the way web server expects it67 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.knownLength79 this._trackLength(header, value, options);80};81 82FormData.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 for88 // 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 stream105 if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) && !(value instanceof Stream))) {106 return;107 }108 109 // no need to bother with the length110 if (!options.knownLength) {111 this._valuesToMeasure.push(value);112 }113};114 115FormData.prototype._lengthRetriever = function(value, callback) {116 117 if (value.hasOwnProperty('fd')) {118 119 // take read range into a account120 // `end` = Infinity –> read file till the end121 //122 // TODO: Looks like there is bug in Node fs.createReadStream123 // it doesn't respect `end` options without `start` options124 // Fix it when node fixes it.125 // https://github.com/joyent/node/issues/7819126 if (value.end != undefined && value.end != Infinity && value.start != undefined) {127 128 // when end specified129 // no need to calculate range130 // inclusive, starts with 0131 callback(null, value.end + 1 - (value.start ? value.start : 0));132 133 // not that fast snoopy134 } else {135 // still need to fetch file size from fs136 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 options146 fileSize = stat.size - (value.start ? value.start : 0);147 callback(null, fileSize);148 });149 }150 151 // or http response152 } else if (value.hasOwnProperty('httpVersion')) {153 callback(null, +value.headers['content-length']);154 155 // or request stream http://github.com/mikeal/request156 } else if (value.hasOwnProperty('httpModule')) {157 // wait till response come back158 value.on('response', function(response) {159 value.pause();160 callback(null, +response.headers['content-length']);161 });162 value.resume();163 164 // something else165 } else {166 callback('Unknown stream');167 }168};169 170FormData.prototype._multiPartHeader = function(field, value, options) {171 // custom header specified (as string)?172 // it becomes responsible for boundary173 // (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 not184 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),185 // if no content type. allow it to be empty array186 '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 218FormData.prototype._getContentDisposition = function(value, options) {219 220 var filename221 , contentDisposition222 ;223 224 if (typeof options.filepath === 'string') {225 // custom filepath for relative paths226 filename = path.normalize(options.filepath).replace(/\\/g, '/');227 } else if (options.filename || value.name || value.path) {228 // custom filename take precedence229 // formidable and the browser add a name property230 // fs- and request- streams have path property231 filename = path.basename(options.filename || value.name || value.path);232 } else if (value.readable && value.hasOwnProperty('httpVersion')) {233 // or try http response234 filename = path.basename(value.client._httpMessage.path || '');235 }236 237 if (filename) {238 contentDisposition = 'filename="' + filename + '"';239 }240 241 return contentDisposition;242};243 244FormData.prototype._getContentType = function(value, options) {245 246 // use custom content-type above all247 var contentType = options.contentType;248 249 // or try `name` from formidable, browser250 if (!contentType && value.name) {251 contentType = mime.lookup(value.name);252 }253 254 // or try `path` from fs-, request- streams255 if (!contentType && value.path) {256 contentType = mime.lookup(value.path);257 }258 259 // or if it's http-reponse260 if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) {261 contentType = value.headers['content-type'];262 }263 264 // or guess it from the filepath or filename265 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 value270 if (!contentType && typeof value == 'object') {271 contentType = FormData.DEFAULT_CONTENT_TYPE;272 }273 274 return contentType;275};276 277FormData.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 290FormData.prototype._lastBoundary = function() {291 return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;292};293 294FormData.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 309FormData.prototype.setBoundary = function(boundary) {310 this._boundary = boundary;311};312 313FormData.prototype.getBoundary = function() {314 if (!this._boundary) {315 this._generateBoundary();316 }317 318 return this._boundary;319};320 321FormData.prototype.getBuffer = function() {322 var dataBuffer = new Buffer.alloc( 0 );323 var boundary = this.getBoundary();324 325 // Create the form content. Add Line breaks to the end of data.326 for (var i = 0, len = this._streams.length; i < len; i++) {327 if (typeof this._streams[i] !== 'function') {328 329 // Add content to the buffer.330 if(Buffer.isBuffer(this._streams[i])) {331 dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]);332 }else {333 dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]);334 }335 336 // Add break after content.337 if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) {338 dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] );339 }340 }341 }342 343 // Add the footer and return the Buffer object.344 return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] );345};346 347FormData.prototype._generateBoundary = function() {348 // This generates a 50 character boundary similar to those used by Firefox.349 // They are optimized for boyer-moore parsing.350 var boundary = '--------------------------';351 for (var i = 0; i < 24; i++) {352 boundary += Math.floor(Math.random() * 10).toString(16);353 }354 355 this._boundary = boundary;356};357 358// Note: getLengthSync DOESN'T calculate streams length359// As workaround one can calculate file size manually360// and add it as knownLength option361FormData.prototype.getLengthSync = function() {362 var knownLength = this._overheadLength + this._valueLength;363 364 // Don't get confused, there are 3 "internal" streams for each keyval pair365 // so it basically checks if there is any value added to the form366 if (this._streams.length) {367 knownLength += this._lastBoundary().length;368 }369 370 // https://github.com/form-data/form-data/issues/40371 if (!this.hasKnownLength()) {372 // Some async length retrievers are present373 // therefore synchronous length calculation is false.374 // Please use getLength(callback) to get proper length375 this._error(new Error('Cannot calculate proper length in synchronous way.'));376 }377 378 return knownLength;379};380 381// Public API to check if length of added values is known382// https://github.com/form-data/form-data/issues/196383// https://github.com/form-data/form-data/issues/262384FormData.prototype.hasKnownLength = function() {385 var hasKnownLength = true;386 387 if (this._valuesToMeasure.length) {388 hasKnownLength = false;389 }390 391 return hasKnownLength;392};393 394FormData.prototype.getLength = function(cb) {395 var knownLength = this._overheadLength + this._valueLength;396 397 if (this._streams.length) {398 knownLength += this._lastBoundary().length;399 }400 401 if (!this._valuesToMeasure.length) {402 process.nextTick(cb.bind(this, null, knownLength));403 return;404 }405 406 asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {407 if (err) {408 cb(err);409 return;410 }411 412 values.forEach(function(length) {413 knownLength += length;414 });415 416 cb(null, knownLength);417 });418};419 420FormData.prototype.submit = function(params, cb) {421 var request422 , options423 , defaults = {method: 'post'}424 ;425 426 // parse provided url if it's string427 // or treat it as options object428 if (typeof params == 'string') {429 430 params = parseUrl(params);431 options = populate({432 port: params.port,433 path: params.pathname,434 host: params.hostname,435 protocol: params.protocol436 }, defaults);437 438 // use custom params439 } else {440 441 options = populate(params, defaults);442 // if no port provided use default one443 if (!options.port) {444 options.port = options.protocol == 'https:' ? 443 : 80;445 }446 }447 448 // put that good code in getHeaders to some use449 options.headers = this.getHeaders(params.headers);450 451 // https if specified, fallback to http in any other case452 if (options.protocol == 'https:') {453 request = https.request(options);454 } else {455 request = http.request(options);456 }457 458 // get content length and fire away459 this.getLength(function(err, length) {460 if (err && err !== 'Unknown stream') {461 this._error(err);462 return;463 }464 465 // add content length466 if (length) {467 request.setHeader('Content-Length', length);468 }469 470 this.pipe(request);471 if (cb) {472 var onResponse;473 474 var callback = function (error, responce) {475 request.removeListener('error', callback);476 request.removeListener('response', onResponse);477 478 return cb.call(this, error, responce);479 };480 481 onResponse = callback.bind(this, null);482 483 request.on('error', callback);484 request.on('response', onResponse);485 }486 }.bind(this));487 488 return request;489};490 491FormData.prototype._error = function(err) {492 if (!this.error) {493 this.error = err;494 this.pause();495 this.emit('error', err);496 }497};498 499FormData.prototype.toString = function () {500 return '[object FormData]';501};502 