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
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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 @@ -97,6 +97,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.

```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
19 changes: 19 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,29 @@ type FastifyCorsPlugin = FastifyPluginCallback<
NonNullable<fastifyCors.FastifyCorsOptions> | fastifyCors.FastifyCorsOptionsDelegate
>;

type FastifyCorsHook =
| 'onRequest'
| 'preParsing'
| 'preValidation'
| 'preHandler'
| 'preSerialization'
| 'onSend'
| 'onError'
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved

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
95 changes: 76 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
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,51 @@ const defaultOptions = {
strictPreflight: true
}

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

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 +86,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 +132,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