From 790e810996a5e2592de72883d80434c46e269986 Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Fri, 4 Nov 2022 11:17:05 -0400 Subject: [PATCH] feature: allow custom hook name --- README.md | 54 +++++++++++++++++++++++++- index.js | 19 ++++++---- test/cors.test.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8bd8a2f3..28b67cc9 100644 --- a/README.md +++ b/README.md @@ -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' @@ -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 `hookName` in the options. + +```js +import Fastify from 'fastify' +import cors from '@fastify/cors' + +const fastify = Fastify() +await fastify.register(cors, { + hookName: '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'), { + hookName: '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). diff --git a/index.js b/index.js index b923caae..26af4082 100644 --- a/index.js +++ b/index.js @@ -19,17 +19,22 @@ const defaultOptions = { strictPreflight: true } +const defaultHookName = '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.hookName || defaultHookName, function handleCors (req, reply, next) { + handler(fastify, corsOptions, req, reply, next) }) } @@ -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, { hookName } = { hookName: defaultHookName }) { + fastify.addHook(hookName, function handleCors (req, reply, next) { if (optionsResolver.length === 2) { handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next) return @@ -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 => handler(fastify, corsOptions, req, reply, next)).catch(next) return } } @@ -76,12 +81,12 @@ function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, repl next(err) } else { const corsOptions = Object.assign({}, defaultOptions, options) - onRequest(fastify, corsOptions, req, reply, next) + handler(fastify, corsOptions, req, reply, next) } }) } -function onRequest (fastify, options, req, reply, next) { +function handler (fastify, options, req, reply, next) { // Always set Vary header // https://github.com/rs/cors/issues/10 addOriginToVaryHeader(reply) diff --git a/test/cors.test.js b/test/cors.test.js index 0c52eab4..0d91e713 100644 --- a/test/cors.test.js +++ b/test/cors.test.js @@ -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, { + hookName: '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)