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.

485 lines
9.7 KiB

  1. /**
  2. * Parted (https://github.com/chjj/parted)
  3. * A streaming multipart state parser.
  4. * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed)
  5. */
  6. var fs = require('fs')
  7. , path = require('path')
  8. , EventEmitter = require('events').EventEmitter
  9. , StringDecoder = require('string_decoder').StringDecoder
  10. , set = require('qs').set
  11. , each = Array.prototype.forEach;
  12. /**
  13. * Character Constants
  14. */
  15. var DASH = '-'.charCodeAt(0)
  16. , CR = '\r'.charCodeAt(0)
  17. , LF = '\n'.charCodeAt(0)
  18. , COLON = ':'.charCodeAt(0)
  19. , SPACE = ' '.charCodeAt(0);
  20. /**
  21. * Parser
  22. */
  23. var Parser = function(type, options) {
  24. if (!(this instanceof Parser)) {
  25. return new Parser(type, options);
  26. }
  27. EventEmitter.call(this);
  28. this.writable = true;
  29. this.readable = true;
  30. this.options = options || {};
  31. var key = grab(type, 'boundary');
  32. if (!key) {
  33. return this._error('No boundary key found.');
  34. }
  35. this.key = Buffer.allocUnsafe('\r\n--' + key);
  36. this._key = {};
  37. each.call(this.key, function(ch) {
  38. this._key[ch] = true;
  39. }, this);
  40. this.state = 'start';
  41. this.pending = 0;
  42. this.written = 0;
  43. this.writtenDisk = 0;
  44. this.buff = Buffer.allocUnsafe(200);
  45. this.preamble = true;
  46. this.epilogue = false;
  47. this._reset();
  48. };
  49. Parser.prototype.__proto__ = EventEmitter.prototype;
  50. /**
  51. * Parsing
  52. */
  53. Parser.prototype.write = function(data) {
  54. if (!this.writable
  55. || this.epilogue) return;
  56. try {
  57. this._parse(data);
  58. } catch (e) {
  59. this._error(e);
  60. }
  61. return true;
  62. };
  63. Parser.prototype.end = function(data) {
  64. if (!this.writable) return;
  65. if (data) this.write(data);
  66. if (!this.epilogue) {
  67. return this._error('Message underflow.');
  68. }
  69. return true;
  70. };
  71. Parser.prototype._parse = function(data) {
  72. var i = 0
  73. , len = data.length
  74. , buff = this.buff
  75. , key = this.key
  76. , ch
  77. , val
  78. , j;
  79. for (; i < len; i++) {
  80. if (this.pos >= 200) {
  81. return this._error('Potential buffer overflow.');
  82. }
  83. ch = data[i];
  84. switch (this.state) {
  85. case 'start':
  86. switch (ch) {
  87. case DASH:
  88. this.pos = 3;
  89. this.state = 'key';
  90. break;
  91. default:
  92. break;
  93. }
  94. break;
  95. case 'key':
  96. if (this.pos === key.length) {
  97. this.state = 'key_end';
  98. i--;
  99. } else if (ch !== key[this.pos]) {
  100. if (this.preamble) {
  101. this.state = 'start';
  102. i--;
  103. } else {
  104. this.state = 'body';
  105. val = this.pos - i;
  106. if (val > 0) {
  107. this._write(key.slice(0, val));
  108. }
  109. i--;
  110. }
  111. } else {
  112. this.pos++;
  113. }
  114. break;
  115. case 'key_end':
  116. switch (ch) {
  117. case CR:
  118. this.state = 'key_line_end';
  119. break;
  120. case DASH:
  121. this.state = 'key_dash_end';
  122. break;
  123. default:
  124. return this._error('Expected CR or DASH.');
  125. }
  126. break;
  127. case 'key_line_end':
  128. switch (ch) {
  129. case LF:
  130. if (this.preamble) {
  131. this.preamble = false;
  132. } else {
  133. this._finish();
  134. }
  135. this.state = 'header_name';
  136. this.pos = 0;
  137. break;
  138. default:
  139. return this._error('Expected CR.');
  140. }
  141. break;
  142. case 'key_dash_end':
  143. switch (ch) {
  144. case DASH:
  145. this.epilogue = true;
  146. this._finish();
  147. return;
  148. default:
  149. return this._error('Expected DASH.');
  150. }
  151. break;
  152. case 'header_name':
  153. switch (ch) {
  154. case COLON:
  155. this.header = buff.toString('ascii', 0, this.pos);
  156. this.pos = 0;
  157. this.state = 'header_val';
  158. break;
  159. default:
  160. buff[this.pos++] = ch | 32;
  161. break;
  162. }
  163. break;
  164. case 'header_val':
  165. switch (ch) {
  166. case CR:
  167. this.state = 'header_val_end';
  168. break;
  169. case SPACE:
  170. if (this.pos === 0) {
  171. break;
  172. }
  173. ; // FALL-THROUGH
  174. default:
  175. buff[this.pos++] = ch;
  176. break;
  177. }
  178. break;
  179. case 'header_val_end':
  180. switch (ch) {
  181. case LF:
  182. val = buff.toString('ascii', 0, this.pos);
  183. this._header(this.header, val);
  184. this.pos = 0;
  185. this.state = 'header_end';
  186. break;
  187. default:
  188. return this._error('Expected LF.');
  189. }
  190. break;
  191. case 'header_end':
  192. switch (ch) {
  193. case CR:
  194. this.state = 'head_end';
  195. break;
  196. default:
  197. this.state = 'header_name';
  198. i--;
  199. break;
  200. }
  201. break;
  202. case 'head_end':
  203. switch (ch) {
  204. case LF:
  205. this.state = 'body';
  206. i++;
  207. if (i >= len) return;
  208. data = data.slice(i);
  209. i = -1;
  210. len = data.length;
  211. break;
  212. default:
  213. return this._error('Expected LF.');
  214. }
  215. break;
  216. case 'body':
  217. switch (ch) {
  218. case CR:
  219. if (i > 0) {
  220. this._write(data.slice(0, i));
  221. }
  222. this.pos = 1;
  223. this.state = 'key';
  224. data = data.slice(i);
  225. i = 0;
  226. len = data.length;
  227. break;
  228. default:
  229. // boyer-moore-like algorithm
  230. // at felixge's suggestion
  231. while ((j = i + key.length - 1) < len) {
  232. if (this._key[data[j]]) break;
  233. i = j;
  234. }
  235. break;
  236. }
  237. break;
  238. }
  239. }
  240. if (this.state === 'body') {
  241. this._write(data);
  242. }
  243. };
  244. Parser.prototype._header = function(name, val) {
  245. /*if (name === 'content-disposition') {
  246. this.field = grab(val, 'name');
  247. this.file = grab(val, 'filename');
  248. if (this.file) {
  249. this.data = stream(this.file, this.options.path);
  250. } else {
  251. this.decode = new StringDecoder('utf8');
  252. this.data = '';
  253. }
  254. }*/
  255. return this.emit('header', name, val);
  256. };
  257. Parser.prototype._write = function(data) {
  258. /*if (this.data == null) {
  259. return this._error('No disposition.');
  260. }
  261. if (this.file) {
  262. this.data.write(data);
  263. this.writtenDisk += data.length;
  264. } else {
  265. this.data += this.decode.write(data);
  266. this.written += data.length;
  267. }*/
  268. this.emit('data', data);
  269. };
  270. Parser.prototype._reset = function() {
  271. this.pos = 0;
  272. this.decode = null;
  273. this.field = null;
  274. this.data = null;
  275. this.file = null;
  276. this.header = null;
  277. };
  278. Parser.prototype._error = function(err) {
  279. this.destroy();
  280. this.emit('error', typeof err === 'string'
  281. ? new Error(err)
  282. : err);
  283. };
  284. Parser.prototype.destroy = function(err) {
  285. this.writable = false;
  286. this.readable = false;
  287. this._reset();
  288. };
  289. Parser.prototype._finish = function() {
  290. var self = this
  291. , field = this.field
  292. , data = this.data
  293. , file = this.file
  294. , part;
  295. this.pending++;
  296. this._reset();
  297. if (data && data.path) {
  298. part = data.path;
  299. data.end(next);
  300. } else {
  301. part = data;
  302. next();
  303. }
  304. function next() {
  305. if (!self.readable) return;
  306. self.pending--;
  307. self.emit('part', field, part);
  308. if (data && data.path) {
  309. self.emit('file', field, part, file);
  310. }
  311. if (self.epilogue && !self.pending) {
  312. self.emit('end');
  313. self.destroy();
  314. }
  315. }
  316. };
  317. /**
  318. * Uploads
  319. */
  320. Parser.root = process.platform === 'win32'
  321. ? 'C:/Temp'
  322. : '/tmp';
  323. /**
  324. * Middleware
  325. */
  326. Parser.middleware = function(options) {
  327. options = options || {};
  328. return function(req, res, next) {
  329. if (options.ensureBody) {
  330. req.body = {};
  331. }
  332. if (req.method === 'GET'
  333. || req.method === 'HEAD'
  334. || req._multipart) return next();
  335. req._multipart = true;
  336. var type = req.headers['content-type'];
  337. if (type) type = type.split(';')[0].trim().toLowerCase();
  338. if (type === 'multipart/form-data') {
  339. Parser.handle(req, res, next, options);
  340. } else {
  341. next();
  342. }
  343. };
  344. };
  345. /**
  346. * Handler
  347. */
  348. Parser.handle = function(req, res, next, options) {
  349. var parser = new Parser(req.headers['content-type'], options)
  350. , diskLimit = options.diskLimit
  351. , limit = options.limit
  352. , parts = {}
  353. , files = {};
  354. parser.on('error', function(err) {
  355. req.destroy();
  356. next(err);
  357. });
  358. parser.on('part', function(field, part) {
  359. set(parts, field, part);
  360. });
  361. parser.on('file', function(field, path, name) {
  362. set(files, field, {
  363. path: path,
  364. name: name,
  365. toString: function() {
  366. return path;
  367. }
  368. });
  369. });
  370. parser.on('data', function() {
  371. if (this.writtenDisk > diskLimit || this.written > limit) {
  372. this.emit('error', new Error('Overflow.'));
  373. this.destroy();
  374. }
  375. });
  376. parser.on('end', next);
  377. req.body = parts;
  378. req.files = files;
  379. req.pipe(parser);
  380. };
  381. /**
  382. * Helpers
  383. */
  384. var isWindows = process.platform === 'win32';
  385. var stream = function(name, dir) {
  386. var ext = path.extname(name) || ''
  387. , name = path.basename(name, ext) || ''
  388. , dir = dir || Parser.root
  389. , tag;
  390. tag = Math.random().toString(36).substring(2);
  391. name = name.substring(0, 200) + '.' + tag;
  392. name = path.join(dir, name) + ext.substring(0, 6);
  393. name = name.replace(/\0/g, '');
  394. if (isWindows) {
  395. name = name.replace(/[:*<>|"?]/g, '');
  396. }
  397. return fs.createWriteStream(name);
  398. };
  399. var grab = function(str, name) {
  400. if (!str) return;
  401. var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i')
  402. , cap = rx.exec(str);
  403. if (cap) {
  404. return cap[1].trim().replace(/^['"]|['"]$/g, '');
  405. }
  406. };
  407. /**
  408. * Expose
  409. */
  410. module.exports = Parser;