Skip to content
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

feature: allow custom hook name #234

Merged
merged 12 commits into from
Nov 8, 2022
55 changes: 54 additions & 1 deletion README.md
Expand Up @@ -19,7 +19,7 @@ npm i @fastify/cors
```

## Usage
Require `@fastify/cors` and register it as any other plugin, it will add a `preHandler` hook and a [wildcard options route](https://github.com/fastify/fastify/issues/326#issuecomment-411360862).
Require `@fastify/cors` and register it as any other plugin, it will add a `onRequest` hook and a [wildcard options route](https://github.com/fastify/fastify/issues/326#issuecomment-411360862).
```js
import Fastify from 'fastify'
import cors from '@fastify/cors'
Expand Down Expand Up @@ -56,6 +56,7 @@ You can use it as is without passing any option or you can configure it as expla
}
```
* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
* `hook`: See the section `Custom Fastify hook name` (default: `onResponse`)
* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Type,Authorization'`) or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Range,X-Content-Range'`) or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
Expand Down Expand Up @@ -97,6 +98,58 @@ fastify.register(async function (fastify) {
fastify.listen({ port: 3000 })
```

### Custom Fastify hook name

By default, `@fastify/cors` adds a `onRequest` hook where the validation and header injection are executed. This can be customized by passing `hook` in the options. Valid values are `onRequest`, `preParsing`, `preValidation`, `preHandler`, `preSerialization`, and `onSend`.

```js
import Fastify from 'fastify'
import cors from '@fastify/cors'

const fastify = Fastify()
await fastify.register(cors, {
hook: 'preHandler',
})

fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})

await fastify.listen({ port: 3000 })
```

When configuring CORS asynchronously, an object with `delegator` key is expected:

```js
const fastify = require('fastify')()

fastify.register(require('@fastify/cors'), {
hook: 'preHandler',
delegator: (req, callback) => {
const corsOptions = {
// This is NOT recommended for production as it enables reflection exploits
origin: true
};

// do not include CORS headers for requests from localhost
if (/^localhost$/m.test(req.headers.origin)) {
corsOptions.origin = false
}

// callback expects two parameters: error and options
callback(null, corsOptions)
},
})

fastify.register(async function (fastify) {
fastify.get('/', (req, reply) => {
reply.send({ hello: 'world' })
})
})

fastify.listen({ port: 3000 })
```

## Acknowledgements

The code is a port for Fastify of [`expressjs/cors`](https://github.com/expressjs/cors).
Expand Down
18 changes: 18 additions & 0 deletions index.d.ts
Expand Up @@ -13,10 +13,28 @@ type FastifyCorsPlugin = FastifyPluginCallback<
NonNullable<fastifyCors.FastifyCorsOptions> | fastifyCors.FastifyCorsOptionsDelegate
>;

type FastifyCorsHook =
| 'onRequest'
| 'preParsing'
| 'preValidation'
| 'preHandler'
| 'preSerialization'
| 'onSend'

declare namespace fastifyCors {
export type OriginFunction = (origin: string, callback: OriginCallback) => void;

export interface FastifyCorsOptions {
/**
* Configures the Lifecycle Hook.
*/
hook?: FastifyCorsHook;

/**
* Configures the delegate function.
*/
delegator?: FastifyCorsOptionsDelegate;

/**
* Configures the Access-Control-Allow-Origin CORS header.
*/
Expand Down
94 changes: 75 additions & 19 deletions index.js
Expand Up @@ -9,6 +9,7 @@ const {
const defaultOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
hook: 'onRequest',
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: false,
Expand All @@ -19,18 +20,50 @@ const defaultOptions = {
strictPreflight: true
}

const validHooks = [
'onRequest',
'preParsing',
'preValidation',
'preHandler',
'preSerialization',
'onSend'
]

const hookWithPayload = [
'preSerialization',
'preParsing',
'onSend'
]

function validateHook (value, next) {
if (validHooks.indexOf(value) !== -1) {
return
}
next(new TypeError('@fastify/cors: Invalid hook option provided.'))
}

function fastifyCors (fastify, opts, next) {
fastify.decorateRequest('corsPreflightEnabled', false)

let hideOptionsRoute = true
if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify)
handleCorsOptionsDelegator(opts, fastify, { hook: defaultOptions.hook }, next)
} else if (opts.delegator) {
const { delegator, ...options } = opts
handleCorsOptionsDelegator(delegator, fastify, options, next)
} else {
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
const corsOptions = Object.assign({}, defaultOptions, opts)
fastify.addHook('onRequest', function onRequestCors (req, reply, next) {
onRequest(fastify, corsOptions, req, reply, next)
})
validateHook(corsOptions.hook, next)
if (hookWithPayload.indexOf(corsOptions.hook) !== -1) {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, payload, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
} else {
fastify.addHook(corsOptions.hook, function handleCors (req, reply, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
}
}

// The preflight reply must occur in the hook. This allows fastify-cors to reply to
Expand All @@ -52,22 +85,44 @@ function fastifyCors (fastify, opts, next) {
next()
}

function handleCorsOptionsDelegator (optionsResolver, fastify) {
fastify.addHook('onRequest', function onRequestCors (req, reply, next) {
if (optionsResolver.length === 2) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
return
function handleCorsOptionsDelegator (optionsResolver, fastify, opts, next) {
const hook = (opts && opts.hook) || defaultOptions.hook
validateHook(hook, next)
if (optionsResolver.length === 2) {
if (hookWithPayload.indexOf(hook) !== -1) {
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
})
} else {
fastify.addHook(hook, function handleCors (req, reply, next) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
})
}
} else {
if (hookWithPayload.indexOf(hook) !== -1) {
// handle delegator based on Promise
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => Object.assign({}, defaultOptions, options))
.then(corsOptions => onRequest(fastify, corsOptions, req, reply, next)).catch(next)
return
}
fastify.addHook(hook, function handleCors (req, reply, payload, next) {
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => Object.assign({}, defaultOptions, options))
.then(corsOptions => addCorsHeadersHandler(fastify, corsOptions, req, reply, next)).catch(next)
return
}
next(new Error('Invalid CORS origin option'))
})
} else {
// handle delegator based on Promise
fastify.addHook(hook, function handleCors (req, reply, next) {
const ret = optionsResolver(req)
if (ret && typeof ret.then === 'function') {
ret.then(options => Object.assign({}, defaultOptions, options))
.then(corsOptions => addCorsHeadersHandler(fastify, corsOptions, req, reply, next)).catch(next)
return
}
next(new Error('Invalid CORS origin option'))
})
}
next(new Error('Invalid CORS origin option'))
})
}
}

function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, reply, next) {
Expand All @@ -76,15 +131,16 @@ function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, repl
next(err)
} else {
const corsOptions = Object.assign({}, defaultOptions, options)
onRequest(fastify, corsOptions, req, reply, next)
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
}
})
}

function onRequest (fastify, options, req, reply, next) {
function addCorsHeadersHandler (fastify, options, req, reply, next) {
// Always set Vary header
// https://github.com/rs/cors/issues/10
addOriginToVaryHeader(reply)

const resolveOriginOption = typeof options.origin === 'function' ? resolveOriginWrapper(fastify, options.origin) : (_, cb) => cb(null, options.origin)

resolveOriginOption(req, (error, resolvedOriginOption) => {
Expand Down