| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- /* eslint-disable class-methods-use-this */
- /* eslint-disable no-underscore-dangle */
- import os from 'node:os';
- import path from 'node:path';
- import fsPromises from 'node:fs/promises';
- import { EventEmitter } from 'node:events';
- import { StringDecoder } from 'node:string_decoder';
- import { hexoid } from 'hexoid';
- import once from 'once';
- import dezalgo from 'dezalgo';
- import { octetstream, querystring, multipart, json } from './plugins/index.js';
- import PersistentFile from './PersistentFile.js';
- import VolatileFile from './VolatileFile.js';
- import DummyParser from './parsers/Dummy.js';
- import MultipartParser from './parsers/Multipart.js';
- import * as errors from './FormidableError.js';
- import FormidableError from './FormidableError.js';
- const toHexoId = hexoid(25);
- const DEFAULT_OPTIONS = {
- maxFields: 1000,
- maxFieldsSize: 20 * 1024 * 1024,
- maxFiles: Infinity,
- maxFileSize: 200 * 1024 * 1024,
- maxTotalFileSize: undefined,
- minFileSize: 1,
- allowEmptyFiles: false,
- createDirsFromUploads: false,
- keepExtensions: false,
- encoding: 'utf-8',
- hashAlgorithm: false,
- uploadDir: os.tmpdir(),
- enabledPlugins: [octetstream, querystring, multipart, json],
- fileWriteStreamHandler: null,
- defaultInvalidName: 'invalid-name',
- filter(_part) {
- return true;
- },
- filename: undefined,
- };
- function hasOwnProp(obj, key) {
- return Object.prototype.hasOwnProperty.call(obj, key);
- }
- const decorateForceSequential = function (promiseCreator) {
- /* forces a function that returns a promise to be sequential
- useful for fs for example */
- let lastPromise = Promise.resolve();
- return async function (...x) {
- const promiseWeAreWaitingFor = lastPromise;
- let currentPromise;
- let callback;
- // we need to change lastPromise before await anything,
- // otherwise 2 calls might wait the same thing
- lastPromise = new Promise(function (resolve) {
- callback = resolve;
- });
- await promiseWeAreWaitingFor;
- currentPromise = promiseCreator(...x);
- currentPromise.then(callback).catch(callback);
- return currentPromise;
- };
- };
- const createNecessaryDirectoriesAsync = decorateForceSequential(function (filePath) {
- const directoryname = path.dirname(filePath);
- return fsPromises.mkdir(directoryname, { recursive: true });
- });
- const invalidExtensionChar = (c) => {
- const code = c.charCodeAt(0);
- return !(
- code === 46 || // .
- (code >= 48 && code <= 57) ||
- (code >= 65 && code <= 90) ||
- (code >= 97 && code <= 122)
- );
- };
- class IncomingForm extends EventEmitter {
- constructor(options = {}) {
- super();
- this.options = { ...DEFAULT_OPTIONS, ...options };
- if (!this.options.maxTotalFileSize) {
- this.options.maxTotalFileSize = this.options.maxFileSize
- }
- const dir = path.resolve(
- this.options.uploadDir || this.options.uploaddir || os.tmpdir(),
- );
- this.uploaddir = dir;
- this.uploadDir = dir;
- // initialize with null
- [
- 'error',
- 'headers',
- 'type',
- 'bytesExpected',
- 'bytesReceived',
- '_parser',
- 'req',
- ].forEach((key) => {
- this[key] = null;
- });
- this._setUpRename();
- this._flushing = 0;
- this._fieldsSize = 0;
- this._totalFileSize = 0;
- this._plugins = [];
- this.openedFiles = [];
- this.options.enabledPlugins = []
- .concat(this.options.enabledPlugins)
- .filter(Boolean);
- if (this.options.enabledPlugins.length === 0) {
- throw new FormidableError(
- 'expect at least 1 enabled builtin plugin, see options.enabledPlugins',
- errors.missingPlugin,
- );
- }
- this.options.enabledPlugins.forEach((plugin) => {
- this.use(plugin);
- });
- this._setUpMaxFields();
- this._setUpMaxFiles();
- this.ended = undefined;
- this.type = undefined;
- }
- use(plugin) {
- if (typeof plugin !== 'function') {
- throw new FormidableError(
- '.use: expect `plugin` to be a function',
- errors.pluginFunction,
- );
- }
- this._plugins.push(plugin.bind(this));
- return this;
- }
- pause () {
- try {
- this.req.pause();
- } catch (err) {
- // the stream was destroyed
- if (!this.ended) {
- // before it was completed, crash & burn
- this._error(err);
- }
- return false;
- }
- return true;
- }
- resume () {
- try {
- this.req.resume();
- } catch (err) {
- // the stream was destroyed
- if (!this.ended) {
- // before it was completed, crash & burn
- this._error(err);
- }
- return false;
- }
- return true;
- }
- // returns a promise if no callback is provided
- async parse(req, cb) {
- this.req = req;
- let promise;
- // Setup callback first, so we don't miss anything from data events emitted immediately.
- if (!cb) {
- let resolveRef;
- let rejectRef;
- promise = new Promise((resolve, reject) => {
- resolveRef = resolve;
- rejectRef = reject;
- });
- cb = (err, fields, files) => {
- if (err) {
- rejectRef(err);
- } else {
- resolveRef([fields, files]);
- }
- }
- }
- const callback = once(dezalgo(cb));
- this.fields = {};
- const files = {};
- this.on('field', (name, value) => {
- if (this.type === 'multipart' || this.type === 'urlencoded') {
- if (!hasOwnProp(this.fields, name)) {
- this.fields[name] = [value];
- } else {
- this.fields[name].push(value);
- }
- } else {
- this.fields[name] = value;
- }
- });
- this.on('file', (name, file) => {
- if (!hasOwnProp(files, name)) {
- files[name] = [file];
- } else {
- files[name].push(file);
- }
- });
- this.on('error', (err) => {
- callback(err, this.fields, files);
- });
- this.on('end', () => {
- callback(null, this.fields, files);
- });
- // Parse headers and setup the parser, ready to start listening for data.
- await this.writeHeaders(req.headers);
- // Start listening for data.
- req
- .on('error', (err) => {
- this._error(err);
- })
- .on('aborted', () => {
- this.emit('aborted');
- this._error(new FormidableError('Request aborted', errors.aborted));
- })
- .on('data', (buffer) => {
- try {
- this.write(buffer);
- } catch (err) {
- this._error(err);
- }
- })
- .on('end', () => {
- if (this.error) {
- return;
- }
- if (this._parser) {
- this._parser.end();
- }
- });
- if (promise) {
- return promise;
- }
- return this;
- }
- async writeHeaders(headers) {
- this.headers = headers;
- this._parseContentLength();
- await this._parseContentType();
- if (!this._parser) {
- this._error(
- new FormidableError(
- 'no parser found',
- errors.noParser,
- 415, // Unsupported Media Type
- ),
- );
- return;
- }
- this._parser.once('error', (error) => {
- this._error(error);
- });
- }
- write(buffer) {
- if (this.error) {
- return null;
- }
- if (!this._parser) {
- this._error(
- new FormidableError('uninitialized parser', errors.uninitializedParser),
- );
- return null;
- }
- this.bytesReceived += buffer.length;
- this.emit('progress', this.bytesReceived, this.bytesExpected);
- this._parser.write(buffer);
- return this.bytesReceived;
- }
- onPart(part) {
- // this method can be overwritten by the user
- return this._handlePart(part);
- }
- async _handlePart(part) {
- if (part.originalFilename && typeof part.originalFilename !== 'string') {
- this._error(
- new FormidableError(
- `the part.originalFilename should be string when it exists`,
- errors.filenameNotString,
- ),
- );
- return;
- }
- // This MUST check exactly for undefined. You can not change it to !part.originalFilename.
- // todo: uncomment when switch tests to Jest
- // console.log(part);
- // ? NOTE(@tunnckocore): no it can be any falsey value, it most probably depends on what's returned
- // from somewhere else. Where recently I changed the return statements
- // and such thing because code style
- // ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field
- // ? NOTE(@tunnckocore): originalFilename is an empty string when a field?
- if (!part.mimetype) {
- let value = '';
- const decoder = new StringDecoder(
- part.transferEncoding || this.options.encoding,
- );
- part.on('data', (buffer) => {
- this._fieldsSize += buffer.length;
- if (this._fieldsSize > this.options.maxFieldsSize) {
- this._error(
- new FormidableError(
- `options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`,
- errors.maxFieldsSizeExceeded,
- 413, // Payload Too Large
- ),
- );
- return;
- }
- value += decoder.write(buffer);
- });
- part.on('end', () => {
- this.emit('field', part.name, value);
- });
- return;
- }
- if (!this.options.filter(part)) {
- return;
- }
- this._flushing += 1;
- let fileSize = 0;
- const newFilename = this._getNewName(part);
- const filepath = this._joinDirectoryName(newFilename);
- const file = await this._newFile({
- newFilename,
- filepath,
- originalFilename: part.originalFilename,
- mimetype: part.mimetype,
- });
- file.on('error', (err) => {
- this._error(err);
- });
- this.emit('fileBegin', part.name, file);
- file.open();
- this.openedFiles.push(file);
- part.on('data', (buffer) => {
- this._totalFileSize += buffer.length;
- fileSize += buffer.length;
- if (this._totalFileSize > this.options.maxTotalFileSize) {
- this._error(
- new FormidableError(
- `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`,
- errors.biggerThanTotalMaxFileSize,
- 413,
- ),
- );
- return;
- }
- if (buffer.length === 0) {
- return;
- }
- this.pause();
- file.write(buffer, () => {
- this.resume();
- });
- });
- part.on('end', () => {
- if (!this.options.allowEmptyFiles && fileSize === 0) {
- this._error(
- new FormidableError(
- `options.allowEmptyFiles is false, file size should be greater than 0`,
- errors.noEmptyFiles,
- 400,
- ),
- );
- return;
- }
- if (fileSize < this.options.minFileSize) {
- this._error(
- new FormidableError(
- `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${fileSize} bytes of file data`,
- errors.smallerThanMinFileSize,
- 400,
- ),
- );
- return;
- }
- if (fileSize > this.options.maxFileSize) {
- this._error(
- new FormidableError(
- `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`,
- errors.biggerThanMaxFileSize,
- 413,
- ),
- );
- return;
- }
- file.end(() => {
- this._flushing -= 1;
- this.emit('file', part.name, file);
- this._maybeEnd();
- });
- });
- }
- // eslint-disable-next-line max-statements
- async _parseContentType() {
- if (this.bytesExpected === 0) {
- this._parser = new DummyParser(this, this.options);
- return;
- }
- if (!this.headers['content-type']) {
- this._error(
- new FormidableError(
- 'bad content-type header, no content-type',
- errors.missingContentType,
- 400,
- ),
- );
- return;
- }
- new DummyParser(this, this.options);
- const results = [];
- await Promise.all(this._plugins.map(async (plugin, idx) => {
- let pluginReturn = null;
- try {
- pluginReturn = await plugin(this, this.options) || this;
- } catch (err) {
- // directly throw from the `form.parse` method;
- // there is no other better way, except a handle through options
- const error = new FormidableError(
- `plugin on index ${idx} failed with: ${err.message}`,
- errors.pluginFailed,
- 500,
- );
- error.idx = idx;
- throw error;
- }
- Object.assign(this, pluginReturn);
- // todo: use Set/Map and pass plugin name instead of the `idx` index
- this.emit('plugin', idx, pluginReturn);
- }));
- this.emit('pluginsResults', results);
- }
- _error(err, eventName = 'error') {
- if (this.error || this.ended) {
- return;
- }
- this.req = null;
- this.error = err;
- this.emit(eventName, err);
- this.openedFiles.forEach((file) => {
- file.destroy();
- });
- }
- _parseContentLength() {
- this.bytesReceived = 0;
- if (this.headers['content-length']) {
- this.bytesExpected = parseInt(this.headers['content-length'], 10);
- } else if (this.headers['transfer-encoding'] === undefined) {
- this.bytesExpected = 0;
- }
- if (this.bytesExpected !== null) {
- this.emit('progress', this.bytesReceived, this.bytesExpected);
- }
- }
- _newParser() {
- return new MultipartParser(this.options);
- }
- async _newFile({ filepath, originalFilename, mimetype, newFilename }) {
- if (this.options.fileWriteStreamHandler) {
- return new VolatileFile({
- newFilename,
- filepath,
- originalFilename,
- mimetype,
- createFileWriteStream: this.options.fileWriteStreamHandler,
- hashAlgorithm: this.options.hashAlgorithm,
- });
- }
- if (this.options.createDirsFromUploads) {
- try {
- await createNecessaryDirectoriesAsync(filepath);
- } catch (errorCreatingDir) {
- this._error(new FormidableError(
- `cannot create directory`,
- errors.cannotCreateDir,
- 409,
- ));
- }
- }
- return new PersistentFile({
- newFilename,
- filepath,
- originalFilename,
- mimetype,
- hashAlgorithm: this.options.hashAlgorithm,
- });
- }
- _getFileName(headerValue) {
- // matches either a quoted-string or a token (RFC 2616 section 19.5.1)
- const m = headerValue.match(
- /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i,
- );
- if (!m) return null;
- const match = m[2] || m[3] || '';
- let originalFilename = match.substr(match.lastIndexOf('\\') + 1);
- originalFilename = originalFilename.replace(/%22/g, '"');
- originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) =>
- String.fromCharCode(code),
- );
- return originalFilename;
- }
- // able to get composed extension with multiple dots
- // "a.b.c" -> ".b.c"
- // as opposed to path.extname -> ".c"
- _getExtension(str) {
- if (!str) {
- return '';
- }
- const basename = path.basename(str);
- const firstDot = basename.indexOf('.');
- const lastDot = basename.lastIndexOf('.');
- let rawExtname = path.extname(basename);
- if (firstDot !== lastDot) {
- rawExtname = basename.slice(firstDot);
- }
- let filtered;
- const firstInvalidIndex = Array.from(rawExtname).findIndex(invalidExtensionChar);
- if (firstInvalidIndex === -1) {
- filtered = rawExtname;
- } else {
- filtered = rawExtname.substring(0, firstInvalidIndex);
- }
- if (filtered === '.') {
- return '';
- }
- return filtered;
- }
- _joinDirectoryName(name) {
- const newPath = path.join(this.uploadDir, name);
- // prevent directory traversal attacks
- if (!newPath.startsWith(this.uploadDir)) {
- return path.join(this.uploadDir, this.options.defaultInvalidName);
- }
- return newPath;
- }
- _setUpRename() {
- const hasRename = typeof this.options.filename === 'function';
- if (hasRename) {
- this._getNewName = (part) => {
- let ext = '';
- let name = this.options.defaultInvalidName;
- if (part.originalFilename) {
- // can be null
- ({ ext, name } = path.parse(part.originalFilename));
- if (this.options.keepExtensions !== true) {
- ext = '';
- }
- }
- return this.options.filename.call(this, name, ext, part, this);
- };
- } else {
- this._getNewName = (part) => {
- const name = toHexoId();
- if (part && this.options.keepExtensions) {
- const originalFilename =
- typeof part === 'string' ? part : part.originalFilename;
- return `${name}${this._getExtension(originalFilename)}`;
- }
- return name;
- };
- }
- }
- _setUpMaxFields() {
- if (this.options.maxFields !== Infinity) {
- let fieldsCount = 0;
- this.on('field', () => {
- fieldsCount += 1;
- if (fieldsCount > this.options.maxFields) {
- this._error(
- new FormidableError(
- `options.maxFields (${this.options.maxFields}) exceeded`,
- errors.maxFieldsExceeded,
- 413,
- ),
- );
- }
- });
- }
- }
- _setUpMaxFiles() {
- if (this.options.maxFiles !== Infinity) {
- let fileCount = 0;
- this.on('fileBegin', () => {
- fileCount += 1;
- if (fileCount > this.options.maxFiles) {
- this._error(
- new FormidableError(
- `options.maxFiles (${this.options.maxFiles}) exceeded`,
- errors.maxFilesExceeded,
- 413,
- ),
- );
- }
- });
- }
- }
- _maybeEnd() {
- if (!this.ended || this._flushing || this.error) {
- return;
- }
- this.req = null;
- this.emit('end');
- }
- }
- export default IncomingForm;
- export { DEFAULT_OPTIONS };
|