buddy

node MVC discord bot
Log | Files | Refs | README

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 };