Skip to content

Commit

Permalink
fix: CORS middleware fixed and refactored (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
LuisEGR authored Jul 21, 2021
1 parent 170e604 commit 2e04fe1
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 48 deletions.
155 changes: 108 additions & 47 deletions middleware/cors.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
// Copyright 2019-2020 Yusuke Sakurai. All rights reserved. MIT license.
import { ServeHandler } from "../server.ts";
import { ServeHandler, ServerRequest } from "../server.ts";
type OriginVerifier = string | RegExp | ((s: string) => boolean);
export interface CORSOptions {
/**
* verifier for Access-Control-Allow-Origin
* Specifies either a single origin, which tells browsers to allow that
* origin to access the resource; or else — for requests without
* credentials — the "*" wildcard, to tell browsers to allow any origin
* to access the resource.
*/
origin: OriginVerifier | OriginVerifier[];
/**
* values for Access-Control-Allow-Method
* Values for Access-Control-Allow-Methods.
* Specifies the method or methods allowed when accessing the resource.
* This is used in response to a preflight request.
*
* @default none
*/
methods?: string[];
/**
* values for Access-Control-Allow-Headers
* Values for Access-Control-Allow-Headers.
* Is used in response to a preflight request to indicate which HTTP headers
* can be used when making the actual request. This header is the server side
* response to the browser's Access-Control-Request-Headers header
* @default none
*/
allowedHeaders?: string[];
/**
* values for Access-Control-Expose-Headers
* Values for Access-Control-Expose-Headers.
* Lets a server whitelist headers that Javascript
* (such as getResponseHeader()) in browsers are allowed to access.
* @default none
*/
exposedHeaders?: string[];
/**
* values for Access-Control-Allow-Credentials
* Value for Access-Control-Allow-Credentials.
* Indicates whether or not the response to the request can be exposed when
* the credentials flag is true. When used as part of a response to a preflight
* request, this indicates whether or not the actual request can be made using
* credentials. Note that simple GET requests are not preflighted, and so if a
* request is made for a resource with credentials, if this header is not
* returned with the resource, the response is ignored by the browser and not
* returned to web content.
* @default none
*/
credentials?: boolean;
withCredentials?: boolean;
/**
* values for Access-Control-Max-Age
* Values for Access-Control-Max-Age.
* Indicates how long the results of a preflight request can be cached.
* @default 0
*/
maxAge?: number;
Expand All @@ -37,51 +57,39 @@ export function cors({
methods = [],
allowedHeaders = [],
exposedHeaders = [],
credentials = false,
withCredentials = false,
maxAge = 0,
}: CORSOptions): ServeHandler {
return (req) => {
if (req.method === "OPTIONS") {
const requestOrigin = req.headers.get("origin");
if (!requestOrigin) {
return;
}
if (origin === "*") {
req.responseHeaders.set("access-control-allow-origin", "*");
} else if (verifyOrigin(origin, requestOrigin)) {
req.responseHeaders.set("access-control-allow-origin", requestOrigin);
} else {
return;
}
const requestMethods = req.headers.get("access-control-request-methods");
if (requestMethods && methods.length > 0) {
const list = requestMethods.split(",").map((v) => v.trim());
const allowed = list.filter((v) => methods.includes(v));
req.responseHeaders.set(
"access-control-allow-methods",
allowed.join(", "),
);
}
const requestHeaders = req.headers.get("access-control-request-headers");
if (requestHeaders && allowedHeaders.length > 0) {
const list = requestHeaders.split(",").map((v) => v.trim());
const allowed = list.filter((v) => allowedHeaders.includes(v));
req.responseHeaders.set(
"access-control-allow-headers",
allowed.join(", "),
);
}
if (exposedHeaders.length > 0) {
req.responseHeaders.set(
"accessl-control-expose-headers",
exposedHeaders.join(", "),
);
}
if (credentials) {
req.responseHeaders.set("access-control-allow-credentials", "true");
}
const requestOrigin = req.headers.get("origin");
if (!requestOrigin) {
return;
}
if (req.method === "OPTIONS") { //preflight
const isValidOrigin = setAccessControlAllowOrigin(
origin,
requestOrigin,
req,
);
if (isValidOrigin === false) return;
setAccessControlRequestMethods(methods, req);
setAccessControlRequestHeaders(allowedHeaders, req);
setAcessControlExposeHeaders(exposedHeaders, req);

req.responseHeaders.set(
"access-control-allow-credentials",
withCredentials.toString(),
);
req.responseHeaders.set("access-control-max-age", `${maxAge}`);

return req.respond({ status: 204 });
} else { //actual response
setAccessControlAllowOrigin(origin, requestOrigin, req);
setAcessControlExposeHeaders(exposedHeaders, req);
req.responseHeaders.set(
"access-control-allow-credentials",
withCredentials.toString(),
);
}
};
}
Expand All @@ -105,3 +113,56 @@ function verifyOrigin(
}
return false;
}

function setAccessControlAllowOrigin(
origin: OriginVerifier | OriginVerifier[],
requestOrigin: string,
req: ServerRequest,
) {
if (origin === "*") {
req.responseHeaders.set("access-control-allow-origin", "*");
} else if (verifyOrigin(origin, requestOrigin)) {
req.responseHeaders.set("access-control-allow-origin", requestOrigin);
} else {
return false;
}
}

function setAccessControlRequestMethods(methods: string[], req: ServerRequest) {
const requestMethods = req.headers.get("access-control-request-methods");
if (requestMethods && methods.length > 0) {
const list = requestMethods.split(",").map((v) => v.trim());
const allowed = list.filter((v) => methods.includes(v));
req.responseHeaders.set(
"access-control-allow-methods",
allowed.join(", "),
);
}
}

function setAcessControlExposeHeaders(
exposedHeaders: string[],
req: ServerRequest,
) {
if (exposedHeaders.length > 0) {
req.responseHeaders.set(
"access-control-expose-headers",
exposedHeaders.join(", "),
);
}
}

function setAccessControlRequestHeaders(
allowedHeaders: string[],
req: ServerRequest,
) {
const requestHeaders = req.headers.get("access-control-request-headers");
if (requestHeaders && allowedHeaders.length > 0) {
const list = requestHeaders.split(",").map((v) => v.trim());
const allowed = list.filter((v) => allowedHeaders.includes(v));
req.responseHeaders.set(
"access-control-allow-headers",
allowed.join(", "),
);
}
}
37 changes: 36 additions & 1 deletion middleware/cors_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ group("cors", (t) => {
methods: ["GET", "HEAD"],
allowedHeaders: ["x-servest-version", "x-deno-version"],
exposedHeaders: ["x-node-version"],
credentials: true,
withCredentials: true,
maxAge: 100,
});
await m(r);
Expand All @@ -45,20 +45,23 @@ group("cors", (t) => {
"100",
);
});

t.test("shouldn't respond if method is not OPTIONS", () => {
const m = cors({ origin: "*" });
const r = createRecorder();
m(r);
assertEquals(r.isResponded(), false);
assertEquals(r.responseHeaders.has("access-control-allow-origin"), false);
});

t.test("shouldn't respond if origin isn't sent", async () => {
const m = cors({ origin: "*" });
const r = createRecorder({ method: "OPTIONS" });
m(r);
assertEquals(r.isResponded(), false);
assertEquals(r.responseHeaders.has("access-control-allow-origin"), false);
});

t.test("shouldn't allow if origin is not verified", async () => {
const m = cors({ origin: "https://servestjs.org" });
const r = createRecorder({
Expand All @@ -71,6 +74,7 @@ group("cors", (t) => {
assertEquals(r.isResponded(), false);
assertEquals(r.responseHeaders.has("access-control-allow-origin"), false);
});

t.test("wildcard", async () => {
const m = cors({ origin: "*" });
const r = createRecorder({
Expand All @@ -83,6 +87,7 @@ group("cors", (t) => {
assertEquals(r.respondedStatus(), 204);
assertEquals(r.responseHeaders.get("access-control-allow-origin"), "*");
});

t.test("verifiers", async () => {
for (
const origin of [
Expand All @@ -109,4 +114,34 @@ group("cors", (t) => {
);
}
});

t.test("Should respond the correct access-control-allow-origin header on other methods", async () => {
const m = cors({ origin: "*" });
const r = createRecorder({
method: "POST",
headers: new Headers({
"origin": "https://servestjs.org",
}),
});
await m(r);
assertEquals(r.responseHeaders.get("access-control-allow-origin"), "*");
});

t.test("Should respond the correct access-control-expose-headers header on other methods", async () => {
const m = cors({
origin: "*",
exposedHeaders: ["x-node-version"],
});
const r = createRecorder({
method: "POST",
headers: new Headers({
"origin": "https://servestjs.org",
}),
});
await m(r);
assertEquals(
r.responseHeaders.get("access-control-expose-headers"),
"x-node-version",
);
});
});

0 comments on commit 2e04fe1

Please # to comment.