-
Notifications
You must be signed in to change notification settings - Fork 204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add cors utils #322
feat: add cors utils #322
Changes from 3 commits
1fc3ce4
1d5f9ed
3ea37e9
4636416
052c215
bb7af39
b87960e
d2c1608
c1eef73
749e478
6f4bdc0
572d6dc
855b59d
9a590e0
2d3dc01
b7ec57e
636eca9
6a6138d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { defineEventHandler } from "../../event"; | ||
import { | ||
resolveCorsOptions, | ||
appendCorsPreflightHeaders, | ||
appendCorsActualRequestHeaders, | ||
isPreflight, | ||
} from "./utils"; | ||
import type { CorsOptions } from "./types"; | ||
|
||
export function handleCors(options: CorsOptions) { | ||
pi0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { | ||
preflight: { statusCode }, | ||
} = resolveCorsOptions(options); | ||
|
||
return defineEventHandler((event) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handle shouldn't create a wrapper handler but directly accept There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: c1eef73 |
||
if (isPreflight(event)) { | ||
appendCorsPreflightHeaders(event, options); | ||
|
||
event.node.res.statusCode = statusCode; | ||
event.node.res.setHeader("Content-Length", "0"); | ||
event.node.res.end(); | ||
} else { | ||
appendCorsActualRequestHeaders(event, options); | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { handleCors } from "./handler"; | ||
export { | ||
isPreflight, | ||
isAllowedOrigin, | ||
appendCorsActualRequestHeaders, | ||
appendCorsPreflightHeaders, | ||
} from "./utils"; | ||
export * from "./types"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { HTTPMethod } from "../../types"; | ||
|
||
export interface CorsOptions { | ||
origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); | ||
methods?: "*" | HTTPMethod[]; | ||
allowHeaders?: "*" | string[]; | ||
exposeHeaders?: "*" | string[]; | ||
credentials?: boolean; | ||
maxAge?: string | false; | ||
preflight?: { | ||
statusCode?: number; | ||
}; | ||
} | ||
|
||
// TODO: Define `ResolvedCorsOptions` as "deep required nonnullable" type of `CorsOptions` | ||
export interface ResolvedCorsOptions { | ||
origin: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); | ||
methods: "*" | HTTPMethod[]; | ||
allowHeaders: "*" | string[]; | ||
exposeHeaders: "*" | string[]; | ||
credentials: boolean; | ||
maxAge: string | false; | ||
preflight: { | ||
statusCode: number; | ||
}; | ||
} | ||
|
||
export type EmptyHeader = Record<string, never>; | ||
|
||
export type AccessControlAllowOriginHeader = | ||
| { | ||
"Access-Control-Allow-Origin": "*"; | ||
} | ||
| { | ||
"Access-Control-Allow-Origin": "null" | string; | ||
Vary: "Origin"; | ||
} | ||
| EmptyHeader; | ||
|
||
export type AccessControlAllowMethodsHeader = | ||
| { | ||
"Access-Control-Allow-Methods": "*" | string; | ||
} | ||
| EmptyHeader; | ||
|
||
export type AccessControlAllowCredentialsHeader = | ||
| { | ||
"Access-Control-Allow-Credentials": "true"; | ||
} | ||
| EmptyHeader; | ||
|
||
export type AccessControlAllowHeadersHeader = | ||
| { | ||
"Access-Control-Allow-Headers": "*" | string; | ||
Vary: "Access-Control-Request-Headers"; | ||
} | ||
| EmptyHeader; | ||
|
||
export type AccessControlExposeHeadersHeader = | ||
| { | ||
"Access-Control-Expose-Headers": "*" | string; | ||
} | ||
| EmptyHeader; | ||
|
||
export type AccessControlMaxAgeHeader = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are exposing this types to end user, prefix would be better. (Do we really need to expose all individuals?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: 052c215 I modified exports and make only |
||
| { | ||
"Access-Control-Max-Age": string; | ||
} | ||
| EmptyHeader; | ||
|
||
export type CorsHeaders = | ||
| AccessControlAllowOriginHeader | ||
| AccessControlAllowMethodsHeader | ||
| AccessControlAllowCredentialsHeader | ||
| AccessControlAllowHeadersHeader | ||
| AccessControlExposeHeadersHeader | ||
| AccessControlMaxAgeHeader; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import { defu } from "defu"; | ||
import { appendHeaders } from "../response"; | ||
import { getMethod, getRequestHeaders, getRequestHeader } from "../request"; | ||
import type { H3Event } from "../../event"; | ||
import type { | ||
CorsOptions, | ||
ResolvedCorsOptions, | ||
AccessControlAllowOriginHeader, | ||
AccessControlAllowMethodsHeader, | ||
AccessControlAllowCredentialsHeader, | ||
AccessControlAllowHeadersHeader, | ||
AccessControlExposeHeadersHeader, | ||
AccessControlMaxAgeHeader, | ||
} from "./types"; | ||
|
||
export function resolveCorsOptions( | ||
options: CorsOptions = {} | ||
): ResolvedCorsOptions { | ||
const defaultOptions: ResolvedCorsOptions = { | ||
origin: "*", | ||
methods: "*", | ||
allowHeaders: "*", | ||
exposeHeaders: "*", | ||
credentials: false, | ||
maxAge: false, | ||
preflight: { | ||
statusCode: 204, | ||
}, | ||
}; | ||
|
||
return defu(options, defaultOptions); | ||
} | ||
|
||
export function isPreflight(event: H3Event): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: 4636416 |
||
const method = getMethod(event); | ||
const origin = getRequestHeader(event, "origin"); | ||
const accessControlRequestMethod = getRequestHeader( | ||
event, | ||
"access-control-request-method" | ||
); | ||
|
||
return method === "OPTIONS" && !!origin && !!accessControlRequestMethod; | ||
} | ||
|
||
export function isAllowedOrigin( | ||
origin: ReturnType<typeof getRequestHeaders>["origin"], | ||
options: CorsOptions | ||
): boolean { | ||
pi0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { origin: originOption } = options; | ||
|
||
if ( | ||
!origin || | ||
!originOption || | ||
originOption === "*" || | ||
originOption === "null" | ||
) { | ||
return true; | ||
} | ||
|
||
if (Array.isArray(originOption)) { | ||
return originOption.some((_origin) => { | ||
if (_origin instanceof RegExp) { | ||
return _origin.test(origin); | ||
} | ||
|
||
return origin === _origin; | ||
}); | ||
} | ||
|
||
return originOption(origin); | ||
} | ||
|
||
export function createOriginHeaders( | ||
event: H3Event, | ||
options: CorsOptions | ||
): AccessControlAllowOriginHeader { | ||
const { origin: originOption } = options; | ||
const origin = getRequestHeader(event, "Origin"); | ||
|
||
if (!origin || !originOption || originOption === "*") { | ||
return { "Access-Control-Allow-Origin": "*" }; | ||
} | ||
|
||
if (typeof originOption === "string") { | ||
return { "Access-Control-Allow-Origin": originOption, Vary: "Origin" }; | ||
} | ||
|
||
return isAllowedOrigin(origin, options) | ||
? { "Access-Control-Allow-Origin": origin, Vary: "Origin" } | ||
: {}; | ||
} | ||
|
||
export function createMethodsHeaders( | ||
options: CorsOptions | ||
): AccessControlAllowMethodsHeader { | ||
const { methods } = options; | ||
|
||
if (!methods) { | ||
return {}; | ||
} | ||
|
||
if (methods === "*") { | ||
return { "Access-Control-Allow-Methods": "*" }; | ||
} | ||
|
||
return methods.length > 0 | ||
? { "Access-Control-Allow-Methods": methods.join(",") } | ||
: {}; | ||
} | ||
|
||
export function createCredentialsHeaders( | ||
options: CorsOptions | ||
): AccessControlAllowCredentialsHeader { | ||
const { credentials } = options; | ||
|
||
if (credentials) { | ||
return { "Access-Control-Allow-Credentials": "true" }; | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
export function createAllowHeaderHeaders( | ||
event: H3Event, | ||
options: CorsOptions | ||
): AccessControlAllowHeadersHeader { | ||
const { allowHeaders } = options; | ||
|
||
if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) { | ||
const header = getRequestHeader(event, "access-control-request-headers"); | ||
|
||
return header | ||
? { | ||
"Access-Control-Allow-Headers": header, | ||
Vary: "Access-Control-Request-Headers", | ||
} | ||
: {}; | ||
} | ||
|
||
return { | ||
"Access-Control-Allow-Headers": allowHeaders.join(","), | ||
Vary: "Access-Control-Request-Headers", | ||
}; | ||
} | ||
|
||
export function createExposeHeaders( | ||
options: CorsOptions | ||
): AccessControlExposeHeadersHeader { | ||
const { exposeHeaders } = options; | ||
|
||
if (!exposeHeaders) { | ||
return {}; | ||
} | ||
|
||
if (exposeHeaders === "*") { | ||
return { "Access-Control-Expose-Headers": exposeHeaders }; | ||
} | ||
|
||
return { "Access-Control-Expose-Headers": exposeHeaders.join(",") }; | ||
} | ||
|
||
export function createMaxAgeHeader( | ||
options: CorsOptions | ||
): AccessControlMaxAgeHeader { | ||
const { maxAge } = options; | ||
|
||
if (maxAge) { | ||
return { "Access-Control-Max-Age": maxAge }; | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
// TODO: Implemente e2e tests to improve code coverage | ||
/* c8 ignore start */ | ||
export function appendCorsPreflightHeaders( | ||
event: H3Event, | ||
options: CorsOptions | ||
) { | ||
appendHeaders(event, createOriginHeaders(event, options)); | ||
appendHeaders(event, createCredentialsHeaders(options)); | ||
appendHeaders(event, createExposeHeaders(options)); | ||
appendHeaders(event, createMethodsHeaders(options)); | ||
appendHeaders(event, createAllowHeaderHeaders(event, options)); | ||
} | ||
/* c8 ignore end */ | ||
|
||
// TODO: Implemente e2e tests to improve code coverage | ||
/* c8 ignore start */ | ||
export function appendCorsActualRequestHeaders( | ||
event: H3Event, | ||
options: CorsOptions | ||
) { | ||
appendHeaders(event, createOriginHeaders(event, options)); | ||
appendHeaders(event, createCredentialsHeaders(options)); | ||
appendHeaders(event, createExposeHeaders(options)); | ||
} | ||
/* c8 ignore end */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me consider it in another time how we can organize README, because it needs lots of words to explain CORS features. Or, we might want to create a Docus website for h3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. If possible adding help in JSDocs would be the best. In the future we should generate README baased on JSDocs and add more. Link is good for me until then 👍🏼