form_data.js 14 KB

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