form_data.js 14 KB

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