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
20 changes: 13 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ const defaultOptions = {
strictPreflight: true
}

const defaultHook = 'onRequest'

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

let hideOptionsRoute = true
if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify)
} else if (opts.delegator) {
const { delegator, ...options } = opts
handleCorsOptionsDelegator(delegator, fastify, options)
} 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)
fastify.addHook(opts.hook || defaultHook, function handleCors (req, reply, next) {
addCorsHeadersHandler(fastify, corsOptions, req, reply, next)
})
}

Expand All @@ -52,8 +57,8 @@ function fastifyCors (fastify, opts, next) {
next()
}

function handleCorsOptionsDelegator (optionsResolver, fastify) {
fastify.addHook('onRequest', function onRequestCors (req, reply, next) {
function handleCorsOptionsDelegator (optionsResolver, fastify, { hook } = { hook: defaultHook }) {
fastify.addHook(hook, function handleCors (req, reply, next) {
if (optionsResolver.length === 2) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
return
Expand All @@ -62,7 +67,7 @@ function handleCorsOptionsDelegator (optionsResolver, fastify) {
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)
.then(corsOptions => addCorsHeadersHandler(fastify, corsOptions, req, reply, next)).catch(next)
return
}
}
Expand All @@ -76,15 +81,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
97 changes: 97 additions & 0 deletions test/cors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,103 @@ test('Should support dynamic config (Promise)', t => {
})
})

test('Should support custom hook with dynamic config', t => {
t.plan(16)

const configs = [{
origin: 'example.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['foo', 'bar'],
allowedHeaders: ['baz', 'woo'],
maxAge: 123
}, {
origin: 'sample.com',
methods: 'GET',
credentials: true,
exposedHeaders: ['zoo', 'bar'],
allowedHeaders: ['baz', 'foo'],
maxAge: 321
}]

const fastify = Fastify()
let requestId = 0
const configDelegation = function (req, cb) {
// request should have id
t.ok(req.id)
// request should not have send
t.notOk(req.send)
const config = configs[requestId]
requestId++
if (config) {
cb(null, config)
} else {
cb(new Error('ouch'))
}
}
fastify.register(cors, {
hook: 'preHandler',
delegator: configDelegation
})

fastify.get('/', (req, reply) => {
reply.send('ok')
})

fastify.inject({
method: 'GET',
url: '/'
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 200)
t.equal(res.payload, 'ok')
t.match(res.headers, {
'access-control-allow-origin': 'example.com',
vary: 'Origin',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'foo, bar',
'content-length': '2'
})
})

fastify.inject({
method: 'OPTIONS',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
delete res.headers.date
t.equal(res.statusCode, 204)
t.equal(res.payload, '')
t.match(res.headers, {
'access-control-allow-origin': 'sample.com',
vary: 'Origin',
'access-control-allow-credentials': 'true',
'access-control-expose-headers': 'zoo, bar',
'access-control-allow-methods': 'GET',
'access-control-allow-headers': 'baz, foo',
'access-control-max-age': '321',
'content-length': '0'
})
})

fastify.inject({
method: 'GET',
url: '/',
headers: {
'access-control-request-method': 'GET',
origin: 'example.com'
}
}, (err, res) => {
t.error(err)
t.equal(res.statusCode, 500)
})
})

test('Should support dynamic config. (Invalid function)', t => {
t.plan(2)

Expand Down