Skip to content

Commit

Permalink
more relaxed boundary validation
Browse files Browse the repository at this point in the history
only throw error when building multipart body bytes
show warning when parsing
  • Loading branch information
zefir-git committed Sep 26, 2024
1 parent 6e19299 commit e60c907
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 33 deletions.
21 changes: 12 additions & 9 deletions src/Multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}

Expand All @@ -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<number>[] = [];
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);
Expand Down Expand Up @@ -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]));
Expand Down Expand Up @@ -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<number>[] = [];
Expand Down
48 changes: 24 additions & 24 deletions test/Multipart.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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();
});
});
});

0 comments on commit e60c907

Please sign in to comment.