form_data.js 9.4 KB

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