Formidable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. /* eslint-disable class-methods-use-this */
  2. /* eslint-disable no-underscore-dangle */
  3. import os from 'node:os';
  4. import path from 'node:path';
  5. import fsPromises from 'node:fs/promises';
  6. import { EventEmitter } from 'node:events';
  7. import { StringDecoder } from 'node:string_decoder';
  8. import { hexoid } from 'hexoid';
  9. import once from 'once';
  10. import dezalgo from 'dezalgo';
  11. import { octetstream, querystring, multipart, json } from './plugins/index.js';
  12. import PersistentFile from './PersistentFile.js';
  13. import VolatileFile from './VolatileFile.js';
  14. import DummyParser from './parsers/Dummy.js';
  15. import MultipartParser from './parsers/Multipart.js';
  16. import * as errors from './FormidableError.js';
  17. import FormidableError from './FormidableError.js';
  18. const toHexoId = hexoid(25);
  19. const DEFAULT_OPTIONS = {
  20. maxFields: 1000,
  21. maxFieldsSize: 20 * 1024 * 1024,
  22. maxFiles: Infinity,
  23. maxFileSize: 200 * 1024 * 1024,
  24. maxTotalFileSize: undefined,
  25. minFileSize: 1,
  26. allowEmptyFiles: false,
  27. createDirsFromUploads: false,
  28. keepExtensions: false,
  29. encoding: 'utf-8',
  30. hashAlgorithm: false,
  31. uploadDir: os.tmpdir(),
  32. enabledPlugins: [octetstream, querystring, multipart, json],
  33. fileWriteStreamHandler: null,
  34. defaultInvalidName: 'invalid-name',
  35. filter(_part) {
  36. return true;
  37. },
  38. filename: undefined,
  39. };
  40. function hasOwnProp(obj, key) {
  41. return Object.prototype.hasOwnProperty.call(obj, key);
  42. }
  43. const decorateForceSequential = function (promiseCreator) {
  44. /* forces a function that returns a promise to be sequential
  45. useful for fs for example */
  46. let lastPromise = Promise.resolve();
  47. return async function (...x) {
  48. const promiseWeAreWaitingFor = lastPromise;
  49. let currentPromise;
  50. let callback;
  51. // we need to change lastPromise before await anything,
  52. // otherwise 2 calls might wait the same thing
  53. lastPromise = new Promise(function (resolve) {
  54. callback = resolve;
  55. });
  56. await promiseWeAreWaitingFor;
  57. currentPromise = promiseCreator(...x);
  58. currentPromise.then(callback).catch(callback);
  59. return currentPromise;
  60. };
  61. };
  62. const createNecessaryDirectoriesAsync = decorateForceSequential(function (filePath) {
  63. const directoryname = path.dirname(filePath);
  64. return fsPromises.mkdir(directoryname, { recursive: true });
  65. });
  66. const invalidExtensionChar = (c) => {
  67. const code = c.charCodeAt(0);
  68. return !(
  69. code === 46 || // .
  70. (code >= 48 && code <= 57) ||
  71. (code >= 65 && code <= 90) ||
  72. (code >= 97 && code <= 122)
  73. );
  74. };
  75. class IncomingForm extends EventEmitter {
  76. constructor(options = {}) {
  77. super();
  78. this.options = { ...DEFAULT_OPTIONS, ...options };
  79. if (!this.options.maxTotalFileSize) {
  80. this.options.maxTotalFileSize = this.options.maxFileSize
  81. }
  82. const dir = path.resolve(
  83. this.options.uploadDir || this.options.uploaddir || os.tmpdir(),
  84. );
  85. this.uploaddir = dir;
  86. this.uploadDir = dir;
  87. // initialize with null
  88. [
  89. 'error',
  90. 'headers',
  91. 'type',
  92. 'bytesExpected',
  93. 'bytesReceived',
  94. '_parser',
  95. 'req',
  96. ].forEach((key) => {
  97. this[key] = null;
  98. });
  99. this._setUpRename();
  100. this._flushing = 0;
  101. this._fieldsSize = 0;
  102. this._totalFileSize = 0;
  103. this._plugins = [];
  104. this.openedFiles = [];
  105. this.options.enabledPlugins = []
  106. .concat(this.options.enabledPlugins)
  107. .filter(Boolean);
  108. if (this.options.enabledPlugins.length === 0) {
  109. throw new FormidableError(
  110. 'expect at least 1 enabled builtin plugin, see options.enabledPlugins',
  111. errors.missingPlugin,
  112. );
  113. }
  114. this.options.enabledPlugins.forEach((plugin) => {
  115. this.use(plugin);
  116. });
  117. this._setUpMaxFields();
  118. this._setUpMaxFiles();
  119. this.ended = undefined;
  120. this.type = undefined;
  121. }
  122. use(plugin) {
  123. if (typeof plugin !== 'function') {
  124. throw new FormidableError(
  125. '.use: expect `plugin` to be a function',
  126. errors.pluginFunction,
  127. );
  128. }
  129. this._plugins.push(plugin.bind(this));
  130. return this;
  131. }
  132. pause () {
  133. try {
  134. this.req.pause();
  135. } catch (err) {
  136. // the stream was destroyed
  137. if (!this.ended) {
  138. // before it was completed, crash & burn
  139. this._error(err);
  140. }
  141. return false;
  142. }
  143. return true;
  144. }
  145. resume () {
  146. try {
  147. this.req.resume();
  148. } catch (err) {
  149. // the stream was destroyed
  150. if (!this.ended) {
  151. // before it was completed, crash & burn
  152. this._error(err);
  153. }
  154. return false;
  155. }
  156. return true;
  157. }
  158. // returns a promise if no callback is provided
  159. async parse(req, cb) {
  160. this.req = req;
  161. let promise;
  162. // Setup callback first, so we don't miss anything from data events emitted immediately.
  163. if (!cb) {
  164. let resolveRef;
  165. let rejectRef;
  166. promise = new Promise((resolve, reject) => {
  167. resolveRef = resolve;
  168. rejectRef = reject;
  169. });
  170. cb = (err, fields, files) => {
  171. if (err) {
  172. rejectRef(err);
  173. } else {
  174. resolveRef([fields, files]);
  175. }
  176. }
  177. }
  178. const callback = once(dezalgo(cb));
  179. this.fields = {};
  180. const files = {};
  181. this.on('field', (name, value) => {
  182. if (this.type === 'multipart' || this.type === 'urlencoded') {
  183. if (!hasOwnProp(this.fields, name)) {
  184. this.fields[name] = [value];
  185. } else {
  186. this.fields[name].push(value);
  187. }
  188. } else {
  189. this.fields[name] = value;
  190. }
  191. });
  192. this.on('file', (name, file) => {
  193. if (!hasOwnProp(files, name)) {
  194. files[name] = [file];
  195. } else {
  196. files[name].push(file);
  197. }
  198. });
  199. this.on('error', (err) => {
  200. callback(err, this.fields, files);
  201. });
  202. this.on('end', () => {
  203. callback(null, this.fields, files);
  204. });
  205. // Parse headers and setup the parser, ready to start listening for data.
  206. await this.writeHeaders(req.headers);
  207. // Start listening for data.
  208. req
  209. .on('error', (err) => {
  210. this._error(err);
  211. })
  212. .on('aborted', () => {
  213. this.emit('aborted');
  214. this._error(new FormidableError('Request aborted', errors.aborted));
  215. })
  216. .on('data', (buffer) => {
  217. try {
  218. this.write(buffer);
  219. } catch (err) {
  220. this._error(err);
  221. }
  222. })
  223. .on('end', () => {
  224. if (this.error) {
  225. return;
  226. }
  227. if (this._parser) {
  228. this._parser.end();
  229. }
  230. });
  231. if (promise) {
  232. return promise;
  233. }
  234. return this;
  235. }
  236. async writeHeaders(headers) {
  237. this.headers = headers;
  238. this._parseContentLength();
  239. await this._parseContentType();
  240. if (!this._parser) {
  241. this._error(
  242. new FormidableError(
  243. 'no parser found',
  244. errors.noParser,
  245. 415, // Unsupported Media Type
  246. ),
  247. );
  248. return;
  249. }
  250. this._parser.once('error', (error) => {
  251. this._error(error);
  252. });
  253. }
  254. write(buffer) {
  255. if (this.error) {
  256. return null;
  257. }
  258. if (!this._parser) {
  259. this._error(
  260. new FormidableError('uninitialized parser', errors.uninitializedParser),
  261. );
  262. return null;
  263. }
  264. this.bytesReceived += buffer.length;
  265. this.emit('progress', this.bytesReceived, this.bytesExpected);
  266. this._parser.write(buffer);
  267. return this.bytesReceived;
  268. }
  269. onPart(part) {
  270. // this method can be overwritten by the user
  271. return this._handlePart(part);
  272. }
  273. async _handlePart(part) {
  274. if (part.originalFilename && typeof part.originalFilename !== 'string') {
  275. this._error(
  276. new FormidableError(
  277. `the part.originalFilename should be string when it exists`,
  278. errors.filenameNotString,
  279. ),
  280. );
  281. return;
  282. }
  283. // This MUST check exactly for undefined. You can not change it to !part.originalFilename.
  284. // todo: uncomment when switch tests to Jest
  285. // console.log(part);
  286. // ? NOTE(@tunnckocore): no it can be any falsey value, it most probably depends on what's returned
  287. // from somewhere else. Where recently I changed the return statements
  288. // and such thing because code style
  289. // ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field
  290. // ? NOTE(@tunnckocore): originalFilename is an empty string when a field?
  291. if (!part.mimetype) {
  292. let value = '';
  293. const decoder = new StringDecoder(
  294. part.transferEncoding || this.options.encoding,
  295. );
  296. part.on('data', (buffer) => {
  297. this._fieldsSize += buffer.length;
  298. if (this._fieldsSize > this.options.maxFieldsSize) {
  299. this._error(
  300. new FormidableError(
  301. `options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`,
  302. errors.maxFieldsSizeExceeded,
  303. 413, // Payload Too Large
  304. ),
  305. );
  306. return;
  307. }
  308. value += decoder.write(buffer);
  309. });
  310. part.on('end', () => {
  311. this.emit('field', part.name, value);
  312. });
  313. return;
  314. }
  315. if (!this.options.filter(part)) {
  316. return;
  317. }
  318. this._flushing += 1;
  319. let fileSize = 0;
  320. const newFilename = this._getNewName(part);
  321. const filepath = this._joinDirectoryName(newFilename);
  322. const file = await this._newFile({
  323. newFilename,
  324. filepath,
  325. originalFilename: part.originalFilename,
  326. mimetype: part.mimetype,
  327. });
  328. file.on('error', (err) => {
  329. this._error(err);
  330. });
  331. this.emit('fileBegin', part.name, file);
  332. file.open();
  333. this.openedFiles.push(file);
  334. part.on('data', (buffer) => {
  335. this._totalFileSize += buffer.length;
  336. fileSize += buffer.length;
  337. if (this._totalFileSize > this.options.maxTotalFileSize) {
  338. this._error(
  339. new FormidableError(
  340. `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`,
  341. errors.biggerThanTotalMaxFileSize,
  342. 413,
  343. ),
  344. );
  345. return;
  346. }
  347. if (buffer.length === 0) {
  348. return;
  349. }
  350. this.pause();
  351. file.write(buffer, () => {
  352. this.resume();
  353. });
  354. });
  355. part.on('end', () => {
  356. if (!this.options.allowEmptyFiles && fileSize === 0) {
  357. this._error(
  358. new FormidableError(
  359. `options.allowEmptyFiles is false, file size should be greater than 0`,
  360. errors.noEmptyFiles,
  361. 400,
  362. ),
  363. );
  364. return;
  365. }
  366. if (fileSize < this.options.minFileSize) {
  367. this._error(
  368. new FormidableError(
  369. `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${fileSize} bytes of file data`,
  370. errors.smallerThanMinFileSize,
  371. 400,
  372. ),
  373. );
  374. return;
  375. }
  376. if (fileSize > this.options.maxFileSize) {
  377. this._error(
  378. new FormidableError(
  379. `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`,
  380. errors.biggerThanMaxFileSize,
  381. 413,
  382. ),
  383. );
  384. return;
  385. }
  386. file.end(() => {
  387. this._flushing -= 1;
  388. this.emit('file', part.name, file);
  389. this._maybeEnd();
  390. });
  391. });
  392. }
  393. // eslint-disable-next-line max-statements
  394. async _parseContentType() {
  395. if (this.bytesExpected === 0) {
  396. this._parser = new DummyParser(this, this.options);
  397. return;
  398. }
  399. if (!this.headers['content-type']) {
  400. this._error(
  401. new FormidableError(
  402. 'bad content-type header, no content-type',
  403. errors.missingContentType,
  404. 400,
  405. ),
  406. );
  407. return;
  408. }
  409. new DummyParser(this, this.options);
  410. const results = [];
  411. await Promise.all(this._plugins.map(async (plugin, idx) => {
  412. let pluginReturn = null;
  413. try {
  414. pluginReturn = await plugin(this, this.options) || this;
  415. } catch (err) {
  416. // directly throw from the `form.parse` method;
  417. // there is no other better way, except a handle through options
  418. const error = new FormidableError(
  419. `plugin on index ${idx} failed with: ${err.message}`,
  420. errors.pluginFailed,
  421. 500,
  422. );
  423. error.idx = idx;
  424. throw error;
  425. }
  426. Object.assign(this, pluginReturn);
  427. // todo: use Set/Map and pass plugin name instead of the `idx` index
  428. this.emit('plugin', idx, pluginReturn);
  429. }));
  430. this.emit('pluginsResults', results);
  431. }
  432. _error(err, eventName = 'error') {
  433. if (this.error || this.ended) {
  434. return;
  435. }
  436. this.req = null;
  437. this.error = err;
  438. this.emit(eventName, err);
  439. this.openedFiles.forEach((file) => {
  440. file.destroy();
  441. });
  442. }
  443. _parseContentLength() {
  444. this.bytesReceived = 0;
  445. if (this.headers['content-length']) {
  446. this.bytesExpected = parseInt(this.headers['content-length'], 10);
  447. } else if (this.headers['transfer-encoding'] === undefined) {
  448. this.bytesExpected = 0;
  449. }
  450. if (this.bytesExpected !== null) {
  451. this.emit('progress', this.bytesReceived, this.bytesExpected);
  452. }
  453. }
  454. _newParser() {
  455. return new MultipartParser(this.options);
  456. }
  457. async _newFile({ filepath, originalFilename, mimetype, newFilename }) {
  458. if (this.options.fileWriteStreamHandler) {
  459. return new VolatileFile({
  460. newFilename,
  461. filepath,
  462. originalFilename,
  463. mimetype,
  464. createFileWriteStream: this.options.fileWriteStreamHandler,
  465. hashAlgorithm: this.options.hashAlgorithm,
  466. });
  467. }
  468. if (this.options.createDirsFromUploads) {
  469. try {
  470. await createNecessaryDirectoriesAsync(filepath);
  471. } catch (errorCreatingDir) {
  472. this._error(new FormidableError(
  473. `cannot create directory`,
  474. errors.cannotCreateDir,
  475. 409,
  476. ));
  477. }
  478. }
  479. return new PersistentFile({
  480. newFilename,
  481. filepath,
  482. originalFilename,
  483. mimetype,
  484. hashAlgorithm: this.options.hashAlgorithm,
  485. });
  486. }
  487. _getFileName(headerValue) {
  488. // matches either a quoted-string or a token (RFC 2616 section 19.5.1)
  489. const m = headerValue.match(
  490. /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i,
  491. );
  492. if (!m) return null;
  493. const match = m[2] || m[3] || '';
  494. let originalFilename = match.substr(match.lastIndexOf('\\') + 1);
  495. originalFilename = originalFilename.replace(/%22/g, '"');
  496. originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) =>
  497. String.fromCharCode(code),
  498. );
  499. return originalFilename;
  500. }
  501. // able to get composed extension with multiple dots
  502. // "a.b.c" -> ".b.c"
  503. // as opposed to path.extname -> ".c"
  504. _getExtension(str) {
  505. if (!str) {
  506. return '';
  507. }
  508. const basename = path.basename(str);
  509. const firstDot = basename.indexOf('.');
  510. const lastDot = basename.lastIndexOf('.');
  511. let rawExtname = path.extname(basename);
  512. if (firstDot !== lastDot) {
  513. rawExtname = basename.slice(firstDot);
  514. }
  515. let filtered;
  516. const firstInvalidIndex = Array.from(rawExtname).findIndex(invalidExtensionChar);
  517. if (firstInvalidIndex === -1) {
  518. filtered = rawExtname;
  519. } else {
  520. filtered = rawExtname.substring(0, firstInvalidIndex);
  521. }
  522. if (filtered === '.') {
  523. return '';
  524. }
  525. return filtered;
  526. }
  527. _joinDirectoryName(name) {
  528. const newPath = path.join(this.uploadDir, name);
  529. // prevent directory traversal attacks
  530. if (!newPath.startsWith(this.uploadDir)) {
  531. return path.join(this.uploadDir, this.options.defaultInvalidName);
  532. }
  533. return newPath;
  534. }
  535. _setUpRename() {
  536. const hasRename = typeof this.options.filename === 'function';
  537. if (hasRename) {
  538. this._getNewName = (part) => {
  539. let ext = '';
  540. let name = this.options.defaultInvalidName;
  541. if (part.originalFilename) {
  542. // can be null
  543. ({ ext, name } = path.parse(part.originalFilename));
  544. if (this.options.keepExtensions !== true) {
  545. ext = '';
  546. }
  547. }
  548. return this.options.filename.call(this, name, ext, part, this);
  549. };
  550. } else {
  551. this._getNewName = (part) => {
  552. const name = toHexoId();
  553. if (part && this.options.keepExtensions) {
  554. const originalFilename =
  555. typeof part === 'string' ? part : part.originalFilename;
  556. return `${name}${this._getExtension(originalFilename)}`;
  557. }
  558. return name;
  559. };
  560. }
  561. }
  562. _setUpMaxFields() {
  563. if (this.options.maxFields !== Infinity) {
  564. let fieldsCount = 0;
  565. this.on('field', () => {
  566. fieldsCount += 1;
  567. if (fieldsCount > this.options.maxFields) {
  568. this._error(
  569. new FormidableError(
  570. `options.maxFields (${this.options.maxFields}) exceeded`,
  571. errors.maxFieldsExceeded,
  572. 413,
  573. ),
  574. );
  575. }
  576. });
  577. }
  578. }
  579. _setUpMaxFiles() {
  580. if (this.options.maxFiles !== Infinity) {
  581. let fileCount = 0;
  582. this.on('fileBegin', () => {
  583. fileCount += 1;
  584. if (fileCount > this.options.maxFiles) {
  585. this._error(
  586. new FormidableError(
  587. `options.maxFiles (${this.options.maxFiles}) exceeded`,
  588. errors.maxFilesExceeded,
  589. 413,
  590. ),
  591. );
  592. }
  593. });
  594. }
  595. }
  596. _maybeEnd() {
  597. if (!this.ended || this._flushing || this.error) {
  598. return;
  599. }
  600. this.req = null;
  601. this.emit('end');
  602. }
  603. }
  604. export default IncomingForm;
  605. export { DEFAULT_OPTIONS };