You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

503 lines
14 KiB

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