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.

325 lines
9.0 KiB

  1. // TODO:
  2. // * support 1 nested multipart level
  3. // (see second multipart example here:
  4. // http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
  5. // * support limits.fieldNameSize
  6. // -- this will require modifications to utils.parseParams
  7. var ReadableStream = require('stream').Readable,
  8. inherits = require('util').inherits;
  9. var Dicer = require('dicer');
  10. var parseParams = require('../utils').parseParams,
  11. decodeText = require('../utils').decodeText,
  12. basename = require('../utils').basename;
  13. var RE_BOUNDARY = /^boundary$/i,
  14. RE_FIELD = /^form-data$/i,
  15. RE_CHARSET = /^charset$/i,
  16. RE_FILENAME = /^filename$/i,
  17. RE_NAME = /^name$/i;
  18. Multipart.detect = /^multipart\/form-data/i;
  19. function Multipart(boy, cfg) {
  20. if (!(this instanceof Multipart))
  21. return new Multipart(boy, cfg);
  22. var i,
  23. len,
  24. self = this,
  25. boundary,
  26. limits = cfg.limits,
  27. parsedConType = cfg.parsedConType || [],
  28. defCharset = cfg.defCharset || 'utf8',
  29. preservePath = cfg.preservePath,
  30. fileopts = (typeof cfg.fileHwm === 'number'
  31. ? { highWaterMark: cfg.fileHwm }
  32. : {});
  33. for (i = 0, len = parsedConType.length; i < len; ++i) {
  34. if (Array.isArray(parsedConType[i])
  35. && RE_BOUNDARY.test(parsedConType[i][0])) {
  36. boundary = parsedConType[i][1];
  37. break;
  38. }
  39. }
  40. function checkFinished() {
  41. if (nends === 0 && finished && !boy._done) {
  42. finished = false;
  43. process.nextTick(function() {
  44. boy._done = true;
  45. boy.emit('finish');
  46. });
  47. }
  48. }
  49. if (typeof boundary !== 'string')
  50. throw new Error('Multipart: Boundary not found');
  51. var fieldSizeLimit = (limits && typeof limits.fieldSize === 'number'
  52. ? limits.fieldSize
  53. : 1 * 1024 * 1024),
  54. fileSizeLimit = (limits && typeof limits.fileSize === 'number'
  55. ? limits.fileSize
  56. : Infinity),
  57. filesLimit = (limits && typeof limits.files === 'number'
  58. ? limits.files
  59. : Infinity),
  60. fieldsLimit = (limits && typeof limits.fields === 'number'
  61. ? limits.fields
  62. : Infinity),
  63. partsLimit = (limits && typeof limits.parts === 'number'
  64. ? limits.parts
  65. : Infinity);
  66. var nfiles = 0,
  67. nfields = 0,
  68. nends = 0,
  69. curFile,
  70. curField,
  71. finished = false;
  72. this._needDrain = false;
  73. this._pause = false;
  74. this._cb = undefined;
  75. this._nparts = 0;
  76. this._boy = boy;
  77. var parserCfg = {
  78. boundary: boundary,
  79. maxHeaderPairs: (limits && limits.headerPairs)
  80. };
  81. if (fileopts.highWaterMark)
  82. parserCfg.partHwm = fileopts.highWaterMark;
  83. if (cfg.highWaterMark)
  84. parserCfg.highWaterMark = cfg.highWaterMark;
  85. this.parser = new Dicer(parserCfg);
  86. this.parser.on('drain', function() {
  87. self._needDrain = false;
  88. if (self._cb && !self._pause) {
  89. var cb = self._cb;
  90. self._cb = undefined;
  91. cb();
  92. }
  93. }).on('part', function onPart(part) {
  94. if (++self._nparts > partsLimit) {
  95. self.parser.removeListener('part', onPart);
  96. self.parser.on('part', skipPart);
  97. boy.hitPartsLimit = true;
  98. boy.emit('partsLimit');
  99. return skipPart(part);
  100. }
  101. // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
  102. // us emit 'end' early since we know the part has ended if we are already
  103. // seeing the next part
  104. if (curField) {
  105. var field = curField;
  106. field.emit('end');
  107. field.removeAllListeners('end');
  108. }
  109. part.on('header', function(header) {
  110. var contype,
  111. fieldname,
  112. parsed,
  113. charset,
  114. encoding,
  115. filename,
  116. nsize = 0;
  117. if (header['content-type']) {
  118. parsed = parseParams(header['content-type'][0]);
  119. if (parsed[0]) {
  120. contype = parsed[0].toLowerCase();
  121. for (i = 0, len = parsed.length; i < len; ++i) {
  122. if (RE_CHARSET.test(parsed[i][0])) {
  123. charset = parsed[i][1].toLowerCase();
  124. break;
  125. }
  126. }
  127. }
  128. }
  129. if (contype === undefined)
  130. contype = 'text/plain';
  131. if (charset === undefined)
  132. charset = defCharset;
  133. if (header['content-disposition']) {
  134. parsed = parseParams(header['content-disposition'][0]);
  135. if (!RE_FIELD.test(parsed[0]))
  136. return skipPart(part);
  137. for (i = 0, len = parsed.length; i < len; ++i) {
  138. if (RE_NAME.test(parsed[i][0])) {
  139. fieldname = decodeText(parsed[i][1], 'binary', 'utf8');
  140. } else if (RE_FILENAME.test(parsed[i][0])) {
  141. filename = decodeText(parsed[i][1], 'binary', 'utf8');
  142. if (!preservePath)
  143. filename = basename(filename);
  144. }
  145. }
  146. } else
  147. return skipPart(part);
  148. if (header['content-transfer-encoding'])
  149. encoding = header['content-transfer-encoding'][0].toLowerCase();
  150. else
  151. encoding = '7bit';
  152. var onData,
  153. onEnd;
  154. if (contype === 'application/octet-stream' || filename !== undefined) {
  155. // file/binary field
  156. if (nfiles === filesLimit) {
  157. if (!boy.hitFilesLimit) {
  158. boy.hitFilesLimit = true;
  159. boy.emit('filesLimit');
  160. }
  161. return skipPart(part);
  162. }
  163. ++nfiles;
  164. if (!boy._events.file) {
  165. self.parser._ignore();
  166. return;
  167. }
  168. ++nends;
  169. var file = new FileStream(fileopts);
  170. curFile = file;
  171. file.on('end', function() {
  172. --nends;
  173. self._pause = false;
  174. checkFinished();
  175. if (self._cb && !self._needDrain) {
  176. var cb = self._cb;
  177. self._cb = undefined;
  178. cb();
  179. }
  180. });
  181. file._read = function(n) {
  182. if (!self._pause)
  183. return;
  184. self._pause = false;
  185. if (self._cb && !self._needDrain) {
  186. var cb = self._cb;
  187. self._cb = undefined;
  188. cb();
  189. }
  190. };
  191. boy.emit('file', fieldname, file, filename, encoding, contype);
  192. onData = function(data) {
  193. if ((nsize += data.length) > fileSizeLimit) {
  194. var extralen = (fileSizeLimit - (nsize - data.length));
  195. if (extralen > 0)
  196. file.push(data.slice(0, extralen));
  197. file.emit('limit');
  198. file.truncated = true;
  199. part.removeAllListeners('data');
  200. } else if (!file.push(data))
  201. self._pause = true;
  202. };
  203. onEnd = function() {
  204. curFile = undefined;
  205. file.push(null);
  206. };
  207. } else {
  208. // non-file field
  209. if (nfields === fieldsLimit) {
  210. if (!boy.hitFieldsLimit) {
  211. boy.hitFieldsLimit = true;
  212. boy.emit('fieldsLimit');
  213. }
  214. return skipPart(part);
  215. }
  216. ++nfields;
  217. ++nends;
  218. var buffer = '',
  219. truncated = false;
  220. curField = part;
  221. onData = function(data) {
  222. if ((nsize += data.length) > fieldSizeLimit) {
  223. var extralen = (fieldSizeLimit - (nsize - data.length));
  224. buffer += data.toString('binary', 0, extralen);
  225. truncated = true;
  226. part.removeAllListeners('data');
  227. } else
  228. buffer += data.toString('binary');
  229. };
  230. onEnd = function() {
  231. curField = undefined;
  232. if (buffer.length)
  233. buffer = decodeText(buffer, 'binary', charset);
  234. boy.emit('field', fieldname, buffer, false, truncated, encoding, contype);
  235. --nends;
  236. checkFinished();
  237. };
  238. }
  239. /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
  240. broken. Streams2/streams3 is a huge black box of confusion, but
  241. somehow overriding the sync state seems to fix things again (and still
  242. seems to work for previous node versions).
  243. */
  244. part._readableState.sync = false;
  245. part.on('data', onData);
  246. part.on('end', onEnd);
  247. }).on('error', function(err) {
  248. if (curFile)
  249. curFile.emit('error', err);
  250. });
  251. }).on('error', function(err) {
  252. boy.emit('error', err);
  253. }).on('finish', function() {
  254. finished = true;
  255. checkFinished();
  256. });
  257. }
  258. Multipart.prototype.write = function(chunk, cb) {
  259. var r;
  260. if ((r = this.parser.write(chunk)) && !this._pause)
  261. cb();
  262. else {
  263. this._needDrain = !r;
  264. this._cb = cb;
  265. }
  266. };
  267. Multipart.prototype.end = function() {
  268. var self = this;
  269. if (this._nparts === 0 && !self._boy._done) {
  270. process.nextTick(function() {
  271. self._boy._done = true;
  272. self._boy.emit('finish');
  273. });
  274. } else if (this.parser.writable)
  275. this.parser.end();
  276. };
  277. function skipPart(part) {
  278. part.resume();
  279. }
  280. function FileStream(opts) {
  281. if (!(this instanceof FileStream))
  282. return new FileStream(opts);
  283. ReadableStream.call(this, opts);
  284. this.truncated = false;
  285. }
  286. inherits(FileStream, ReadableStream);
  287. FileStream.prototype._read = function(n) {};
  288. module.exports = Multipart;