form_data.js 11 KB

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