form_data.js 14 KB

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