Skip to content

Commit

Permalink
feature: allow custom hook name (#234)
Browse files Browse the repository at this point in the history
* feature: allow custom hook name

* add typings

* fix lint issue

* simplify handleCorsOptionsDelegator

* validate hook in simple case

* add more checks for hooks

* use onRespone hook for hook tests

* move hook related test to hooks.test.js

* simplify handleCorsOptionsDelegator

* handle hooks with payload parameter properly

* remove onError

* improve Readme

Co-authored-by: Uzlopak <aras.abbasi@googlemail.com>
  • Loading branch information
giulianok and Uzlopak committed Nov 8, 2022
1 parent 06e16fc commit ad759a7
Show file tree
Hide file tree
Showing 5 changed files with 909 additions and 20 deletions.
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

0 comments on commit ad759a7

Please sign in to comment.