From e60c907b0702dabd4b5a259fbd35137228b21380 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 26 Sep 2024 21:12:34 +0300 Subject: [PATCH] more relaxed boundary validation only throw error when building multipart body bytes show warning when parsing --- src/Multipart.ts | 21 ++++++++++-------- test/Multipart.test.js | 48 +++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/Multipart.ts b/src/Multipart.ts index 65ccce3..6828f9a 100644 --- a/src/Multipart.ts +++ b/src/Multipart.ts @@ -59,14 +59,9 @@ export class Multipart implements Part { * @param parts The parts to include in the multipart * @param [boundary] The multipart boundary used to separate the parts. Randomly generated if not provided * @param [mediaType] The media type of the multipart. Defaults to "multipart/mixed" - * - * @throws {RangeError} If the boundary is invalid. A valid boundary is 1 to 70 characters long, - * does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space */ public constructor(public readonly parts: Part[], boundary: Uint8Array | string = crypto.randomUUID(), mediaType: string = "multipart/mixed") { this.#boundary = typeof boundary === "string" ? new TextEncoder().encode(boundary) : boundary; - if (!Multipart.isValidBoundary(this.#boundary)) - throw new RangeError("Invalid boundary: must be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space"); this.#mediaType = mediaType; this.setHeaders(); } @@ -121,13 +116,9 @@ export class Multipart implements Part { /** * Set the boundary bytes used to separate the parts - * @throws {RangeError} If the boundary is invalid. A valid boundary is 1 to 70 characters long, - * does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space */ public set boundary(boundary: Uint8Array | string) { this.#boundary = typeof boundary === "string" ? new TextEncoder().encode(boundary) : boundary; - if (!Multipart.isValidBoundary(this.#boundary)) - throw new RangeError("Invalid boundary: must be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space"); this.setHeaders(); } @@ -149,8 +140,14 @@ export class Multipart implements Part { /** * Get the bytes of the body of this multipart. Includes all parts separated by the boundary. * Does not include the headers. + * + * @throws {RangeError} If the multipart boundary is invalid. A valid boundary is 1 to 70 characters long, + * does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space */ public get body(): Uint8Array { + if (!Multipart.isValidBoundary(this.#boundary)) + throw new RangeError("Invalid boundary: must be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space"); + const result: ArrayLike[] = []; for (const part of this.parts) result.push(Multipart.DOUBLE_DASH, this.boundary, Multipart.CRLF, part.bytes(), Multipart.CRLF); result.push(Multipart.DOUBLE_DASH, this.boundary, Multipart.DOUBLE_DASH, Multipart.CRLF); @@ -180,6 +177,9 @@ export class Multipart implements Part { * @param [mediaType] Multipart media type to pass to the constructor */ public static parseBody(data: Uint8Array, boundary: Uint8Array, mediaType?: string): Multipart { + if (!Multipart.isValidBoundary(boundary)) + console.warn("Invalid boundary:", new TextDecoder().decode(boundary), "\nMust be 1 to 70 characters long, not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space"); + const parts: Uint8Array[] = []; const fullBoundarySequence = new Uint8Array(Multipart.combineArrays([Multipart.DOUBLE_DASH, boundary, Multipart.CRLF])); const endBoundarySequence = new Uint8Array(Multipart.combineArrays([Multipart.DOUBLE_DASH, boundary, Multipart.DOUBLE_DASH, Multipart.CRLF])); @@ -393,6 +393,9 @@ export class Multipart implements Part { /** * Get the bytes of the headers and {@link body} of this multipart. + * + * @throws {RangeError} If the multipart boundary is invalid. A valid boundary is 1 to 70 characters long, + * does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space */ public bytes(): Uint8Array { const result: ArrayLike[] = []; diff --git a/test/Multipart.test.js b/test/Multipart.test.js index 9f0128d..7587eb6 100644 --- a/test/Multipart.test.js +++ b/test/Multipart.test.js @@ -27,30 +27,6 @@ describe("Multipart", function () { expect(new TextDecoder().decode(multipart.boundary)).to.equal("empty-boundary"); expect(multipart.mediaType).to.equal("multipart/mixed"); }); - - it("should accept only valid boundaries", function () { - expect(() => new Multipart([], "")).to.throw(RangeError, "Invalid boundary"); - expect(() => new Multipart([], " ")).to.throw(RangeError, "Invalid boundary"); - expect(() => new Multipart([], "a ")).to.throw(RangeError, "Invalid boundary"); - expect(() => new Multipart([], "0123456789".repeat(7) + "0")).to.throw(RangeError, "Invalid boundary"); - expect(() => new Multipart([], "foo!bar")).to.throw(RangeError, "Invalid boundary"); - - expect(() => new Multipart([], "a")).to.not.throw(); - expect(() => new Multipart([], "0123456789".repeat(7))).to.not.throw(); - expect(() => new Multipart([], "foo bar")).to.not.throw(); - expect(() => new Multipart([], "foo'bar")).to.not.throw(); - expect(() => new Multipart([], "foo(bar")).to.not.throw(); - expect(() => new Multipart([], "foo)bar")).to.not.throw(); - expect(() => new Multipart([], "foo+bar")).to.not.throw(); - expect(() => new Multipart([], "foo_bar")).to.not.throw(); - expect(() => new Multipart([], "foo,bar")).to.not.throw(); - expect(() => new Multipart([], "foo-bar")).to.not.throw(); - expect(() => new Multipart([], "foo.bar")).to.not.throw(); - expect(() => new Multipart([], "foo/bar")).to.not.throw(); - expect(() => new Multipart([], "foo:bar")).to.not.throw(); - expect(() => new Multipart([], "foo=bar")).to.not.throw(); - expect(() => new Multipart([], "foo?bar")).to.not.throw(); - }); }); describe("parse", function () { @@ -276,5 +252,29 @@ describe("Multipart", function () { expect(new TextDecoder().decode(bytes)).to.equal(new TextDecoder().decode(expectedBytes)); }); + + it("should accept only valid boundaries", function () { + expect(() => new Multipart([], "").bytes()).to.throw(RangeError, "Invalid boundary"); + expect(() => new Multipart([], " ").bytes()).to.throw(RangeError, "Invalid boundary"); + expect(() => new Multipart([], "a ").bytes()).to.throw(RangeError, "Invalid boundary"); + expect(() => new Multipart([], "0123456789".repeat(7) + "0").bytes()).to.throw(RangeError, "Invalid boundary"); + expect(() => new Multipart([], "foo!bar").bytes()).to.throw(RangeError, "Invalid boundary"); + + expect(() => new Multipart([], "a").bytes()).to.not.throw(); + expect(() => new Multipart([], "0123456789".repeat(7)).bytes()).to.not.throw(); + expect(() => new Multipart([], "foo bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo'bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo(bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo)bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo+bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo_bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo,bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo-bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo.bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo/bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo:bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo=bar").bytes()).to.not.throw(); + expect(() => new Multipart([], "foo?bar").bytes()).to.not.throw(); + }); }); });