diff --git a/src/plugins/octetstream.js b/src/plugins/octetstream.js index 8a0b2709..80f082f2 100644 --- a/src/plugins/octetstream.js +++ b/src/plugins/octetstream.js @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle */ import OctetStreamParser from "../parsers/OctetStream.js"; +import FormidableError, * as errors from "../FormidableError.js"; export const octetStreamType = "octet-stream"; // the `options` is also available through the `options` / `formidable.options` @@ -29,6 +30,11 @@ async function init(_self, _opts) { originalFilename, mimetype, }; + + if (!this.options.filter({ name: "file", ...thisPart })) { + return ignoreFile.call(this); + } + const newFilename = this._getNewName(thisPart); const filepath = this._joinDirectoryName(newFilename); const file = await this._newFile({ @@ -63,6 +69,28 @@ async function init(_self, _opts) { }); this._parser.on("end", () => { + const bodySize = this.bytesReceived; + if (!this.options.allowEmptyFiles && bodySize === 0) { + this._error( + new FormidableError( + `options.allowEmptyFiles is false, file size should be greater than 0`, + errors.noEmptyFiles, + 400 + ) + ); + return; + } + if (bodySize < this.options.minFileSize) { + this._error( + new FormidableError( + `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${bodySize} bytes of file data`, + errors.smallerThanMinFileSize, + 400 + ) + ); + return; + } + this._flushing -= 1; this.ended = true; @@ -82,3 +110,13 @@ async function init(_self, _opts) { return this; } + +function ignoreFile() { + this._parser = new OctetStreamParser(this.options); + this._parser.on("end", () => { + this.ended = true; + this._maybeEnd(); + }); + this._parser.resume(); + return this; +} diff --git a/test/integration/octet-stream-file-checks.test.js b/test/integration/octet-stream-file-checks.test.js new file mode 100644 index 00000000..22025e64 --- /dev/null +++ b/test/integration/octet-stream-file-checks.test.js @@ -0,0 +1,133 @@ +import { unlinkSync } from "node:fs"; +import { createServer, request as _request } from "node:http"; +import assert, { strictEqual, deepStrictEqual } from "node:assert"; + +import formidable from "../../src/index.js"; +import { + noEmptyFiles, + smallerThanMinFileSize, +} from "../../src/FormidableError.js"; + +function upload({ port, options, headers, body }, check, done) { + let request; + let parsed = false; + const server = createServer((req, res) => { + const form = formidable(options); + + form.parse(req, (err, fields, files) => { + parsed = true; + res.end(); + request.destroy(); + server.close(); + try { + check(err, files); + done(); + } catch (error) { + done(error); + } finally { + Object.values(files) + .flat() + .forEach((file) => unlinkSync(file.filepath)); + } + }); + }); + + server.listen(port, (err) => { + assert(!err, "should not have error, but be falsey"); + + request = _request({ + port, + method: "POST", + headers: { "Content-Type": "application/octet-stream", ...headers }, + }); + + request.on("error", (error) => { + if (parsed) return; + server.close(); + done(error); + }); + request.end(body); + }); +} + +test("octet stream applies filter", (done) => { + const calls = []; + upload( + { + port: 13552, + options: { + filter: ({ name, originalFilename, mimetype }) => { + calls.push({ name, originalFilename, mimetype }); + return false; + }, + }, + headers: { "X-File-Name": "a.bin" }, + body: Buffer.alloc(1000, 0x42), + }, + (err, files) => { + assert(!err, "a filtered file should not make the parse fail"); + strictEqual(Object.keys(files).length, 0); + deepStrictEqual(calls, [ + { + name: "file", + originalFilename: "a.bin", + mimetype: "application/octet-stream", + }, + ]); + }, + done + ); +}); + +test("octet stream enforces minFileSize", (done) => { + upload( + { + port: 13553, + options: { minFileSize: 1000 }, + body: Buffer.alloc(10, 0x42), + }, + (err, files) => { + assert( + err, + "expected an error for an octet-stream upload under minFileSize" + ); + strictEqual(err.code, smallerThanMinFileSize); + strictEqual(Object.keys(files).length, 0); + }, + done + ); +}); + +test("octet stream rejects an empty chunked body", (done) => { + upload( + { + port: 13554, + options: {}, + headers: { "Transfer-Encoding": "chunked" }, + body: Buffer.alloc(0), + }, + (err, files) => { + assert(err, "expected an error for an empty octet-stream upload"); + strictEqual(err.code, noEmptyFiles); + strictEqual(Object.keys(files).length, 0); + }, + done + ); +}); + +test("octet stream accepts an empty chunked body with allowEmptyFiles", (done) => { + upload( + { + port: 13555, + options: { allowEmptyFiles: true, minFileSize: 0 }, + headers: { "Transfer-Encoding": "chunked" }, + body: Buffer.alloc(0), + }, + (err, files) => { + assert(!err, "an empty file is allowed with allowEmptyFiles"); + strictEqual(files.file.length, 1); + strictEqual(files.file[0].size, 0); + }, + done + ); +});