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.

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