Skip to content

Commit

Permalink
feat: add factory for coop header field
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Apr 9, 2023
1 parent ca640ae commit 9883dce
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 0 deletions.
8 changes: 8 additions & 0 deletions _dev_deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
assert,
assertEquals,
assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";
export { describe, it } from "https://deno.land/[email protected]/testing/bdd.ts";
export { equalsResponse } from "https://deno.land/x/[email protected]/response.ts";
export { CrossOriginOpenerPolicyValue, PolicyHeader } from "./constants.ts";
32 changes: 32 additions & 0 deletions constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

/** Cross-origin embedded policy value.
* @see [cross-origin opener policy value](https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy-value)
*/
export enum CrossOriginOpenerPolicyValue {
/** The document will occupy the same [top-level browsing context](https://html.spec.whatwg.org/multipage/document-sequences.html#top-level-browsing-context) as its predecessor,
* unless that document specified a different [cross-origin opener policy](https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy).
* @see ["unsafe-none"](https://html.spec.whatwg.org/multipage/browsers.html#coop-unsafe-none)
*/
UnsafeNone = "unsafe-none",

/** This forces the creation of a new [top-level browsing context](https://html.spec.whatwg.org/multipage/document-sequences.html#top-level-browsing-context) for the document,
* unless its predecessor specified the same [cross-origin opener policy](https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy) and they are [same origin](https://html.spec.whatwg.org/multipage/browsers.html#same-origin).
* @see ["same-origin-allow-popups"](https://html.spec.whatwg.org/multipage/browsers.html#coop-same-origin-allow-popups)
*/
SameOriginAllowPopups = "same-origin-allow-popups",

/** This behaves the same as {@link CrossOriginOpenerPolicyValue.SameOriginAllowPopups},
* with the addition that any [auxiliary browsing context](https://html.spec.whatwg.org/multipage/document-sequences.html#auxiliary-browsing-context) created needs to contain [same origin](https://html.spec.whatwg.org/multipage/browsers.html#same-origin) documents
* that also have the same [cross-origin opener policy](https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-opener-policy) or it will appear closed to the opener.
* @see ["same-origin"](https://html.spec.whatwg.org/multipage/browsers.html#coop-same-origin)
*/
SameOrigin = "same-origin",
}

export const enum PolicyHeader {
CrossOriginOpenerPolicy = "cross-origin-opener-policy",
CrossOriginOpenerPolicyReportOnly =
`${PolicyHeader.CrossOriginOpenerPolicy}-report-only`,
}
15 changes: 15 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

export {
type Handler,
type Middleware,
} from "https://deno.land/x/[email protected]/mod.ts";
export { withHeader } from "https://deno.land/x/[email protected]/message.ts";
export { isString } from "https://deno.land/x/[email protected]/is_string.ts";
export {
Item,
Parameters,
stringifySfv,
Token,
} from "https://deno.land/x/[email protected]/mod.ts";
64 changes: 64 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

import { CrossOriginOpenerPolicyValue, PolicyHeader } from "./constants.ts";
import { CrossOriginOpenerPolicy } from "./types.ts";
import { type Middleware, withHeader } from "./deps.ts";
import { stringifyCrossOriginOpenerPolicy } from "./utils.ts";

/** Middleware options. */
export interface Options
extends Partial<Pick<CrossOriginOpenerPolicy, "reportTo">> {
/** Opener policy value.
* @default "same-origin"
*/
readonly policy?: `${CrossOriginOpenerPolicyValue}`;

/** Whether header is report-only or not.
* Depending on the value, the header will be:
* - `true`: `Cross-Origin-Opener-Policy-Report-Only`
* - `false`: `Cross-Origin-Opener-Policy`
* @default false
*/
readonly reportOnly?: boolean;
}

/** Create cross-origin opener policy middleware.
*
* @example
* ```ts
* import {
* coop,
* type Handler,
* } from "https://deno.land/x/coop_middleware@$VERSION/mod.ts";
* import { assert } from "https://deno.land/std/testing/asserts.ts";
*
* declare const request: Request;
* declare const handler: Handler;
*
* const middleware = coop();
* const response = await middleware(request, handler);
*
* assert(response.headers.has("cross-origin-opener-policy"));
* ```
*/
export function coop(options?: Options): Middleware {
const {
policy: value = CrossOriginOpenerPolicyValue.SameOrigin,
reportOnly,
reportTo,
} = options ?? {};

const fieldValue = stringifyCrossOriginOpenerPolicy({ value, reportTo });
const fieldName = reportOnly
? PolicyHeader.CrossOriginOpenerPolicyReportOnly
: PolicyHeader.CrossOriginOpenerPolicy;

return async (request, next) => {
const response = await next(request);

if (response.headers.has(fieldName)) return response;

return withHeader(response, fieldName, fieldValue);
};
}
106 changes: 106 additions & 0 deletions middleware_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { coop } from "./middleware.ts";
import {
assert,
assertThrows,
CrossOriginOpenerPolicyValue,
describe,
equalsResponse,
it,
PolicyHeader,
} from "./_dev_deps.ts";

describe("coop", () => {
it("should return same response if the response include the header", async () => {
const middleware = coop();
const initResponse = new Response(null, {
headers: {
[PolicyHeader.CrossOriginOpenerPolicy]: "",
},
});
const response = await middleware(
new Request("test:"),
() => initResponse,
);

assert(response === initResponse);
});

it("should return response what include coop header and the value is same-origin by default", async () => {
const middleware = coop();
const response = await middleware(
new Request("test:"),
() => new Response(),
);

assert(equalsResponse(
response,
new Response(null, {
headers: {
[PolicyHeader.CrossOriginOpenerPolicy]:
CrossOriginOpenerPolicyValue.SameOrigin,
},
}),
));
});

it("should change coop header via arg", async () => {
const middleware = coop({
policy: CrossOriginOpenerPolicyValue.SameOriginAllowPopups,
});
const response = await middleware(
new Request("test:"),
() => new Response(),
);

assert(equalsResponse(
response,
new Response(null, {
headers: {
[PolicyHeader.CrossOriginOpenerPolicy]:
CrossOriginOpenerPolicyValue.SameOriginAllowPopups,
},
}),
));
});

it("should add report-to param via endpoint", async () => {
const reportTo = "default";
const middleware = coop({ reportTo });
const response = await middleware(
new Request("test:"),
() => new Response(),
);

assert(equalsResponse(
response,
new Response(null, {
headers: {
[PolicyHeader.CrossOriginOpenerPolicy]:
`${CrossOriginOpenerPolicyValue.SameOrigin};report-to=${reportTo}`,
},
}),
));
});

it("should change to report only header", async () => {
const middleware = coop({ reportOnly: true });
const response = await middleware(
new Request("test:"),
() => new Response(),
);

assert(equalsResponse(
response,
new Response(null, {
headers: {
[PolicyHeader.CrossOriginOpenerPolicyReportOnly]:
CrossOriginOpenerPolicyValue.SameOrigin,
},
}),
));
});

it("should throw error if the Cross origin opener policy is invalid", () => {
assertThrows(() => coop({ reportTo: "?" }));
});
});
7 changes: 7 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

export { coop, type Options } from "./middleware.ts";
export { CrossOriginOpenerPolicyValue } from "./constants.ts";
export { type Handler, type Middleware } from "./deps.ts";
export type { CrossOriginOpenerPolicy } from "./types.ts";
13 changes: 13 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

import { type CrossOriginOpenerPolicyValue } from "./constants.ts";

/** Cross-origin opener policy API. */
export interface CrossOriginOpenerPolicy {
/** opener policy value. */
readonly value: `${CrossOriginOpenerPolicyValue}`;

/** Reporting endpoint name. */
readonly reportTo?: string;
}
34 changes: 34 additions & 0 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

import type { CrossOriginOpenerPolicy } from "./types.ts";
import { isString, Item, Parameters, stringifySfv, Token } from "./deps.ts";

const enum Param {
ReportTo = "report-to",
}

/** Serialize {@link CrossOriginOpenerPolicy} into string.
* @throws {TypeError} If the {@link CrossOriginOpenerPolicy} is invalid.
*/
export function stringifyCrossOriginOpenerPolicy(
policy: CrossOriginOpenerPolicy,
): string {
const token = new Token(policy.value);
const parameters = isString(policy.reportTo)
? new Parameters({
[Param.ReportTo]: new Token(policy.reportTo),
})
: new Parameters();
const item = new Item([token, parameters]);

try {
return stringifySfv(item);
} catch (cause) {
throw TypeError(Msg.InvalidCrossOriginOpenerPolicy, { cause });
}
}

const enum Msg {
InvalidCrossOriginOpenerPolicy = "invalid CrossOriginOpenerPolicy format.",
}

0 comments on commit 9883dce

Please sign in to comment.