You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

218 lines
6.4 KiB

3 years ago
'use strict'
const fp = require('fastify-plugin')
const vary = require('./vary')
const defaultOptions = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: false,
exposedHeaders: null,
allowedHeaders: null,
maxAge: null,
preflight: true,
strictPreflight: true
}
function fastifyCors (fastify, opts, next) {
fastify.decorateRequest('corsPreflightEnabled', false)
let hideOptionsRoute = true
if (typeof opts === 'function') {
handleCorsOptionsDelegator(opts, fastify)
} else {
hideOptionsRoute = opts.hideOptionsRoute
const corsOptions = Object.assign({}, defaultOptions, opts)
fastify.addHook('onRequest', (req, reply, next) => {
onRequest(fastify, corsOptions, req, reply, next)
})
}
// The preflight reply must occur in the hook. This allows fastify-cors to reply to
// preflight requests BEFORE possible authentication plugins. If the preflight reply
// occurred in this handler, other plugins may deny the request since the browser will
// remove most headers (such as the Authentication header).
//
// This route simply enables fastify to accept preflight requests.
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => {
if (!req.corsPreflightEnabled) {
// Do not handle preflight requests if the origin option disabled CORS
reply.callNotFound()
return
}
reply.send()
})
next()
}
function handleCorsOptionsDelegator (optionsResolver, fastify) {
fastify.addHook('onRequest', (req, reply, next) => {
if (optionsResolver.length === 2) {
handleCorsOptionsCallbackDelegator(optionsResolver, fastify, req, reply, next)
return
} else {
// handle delegator based on Promise
const ret = optionsResolver(reply)
if (ret && typeof ret.then === 'function') {
ret.then(options => Object.assign({}, defaultOptions, options))
.then(corsOptions => onRequest(fastify, corsOptions, req, reply, next)).catch(next)
return
}
}
next(new Error('Invalid CORS origin option'))
})
}
function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, reply, next) {
optionsResolver(req, (err, options) => {
if (err) {
next(err)
} else {
const corsOptions = Object.assign({}, defaultOptions, options)
onRequest(fastify, corsOptions, req, reply, next)
}
})
}
function onRequest (fastify, options, req, reply, next) {
// Always set Vary header
// https://github.com/rs/cors/issues/10
vary(reply, 'Origin')
const resolveOriginOption = typeof options.origin === 'function' ? resolveOriginWrapper(fastify, options.origin) : (_, cb) => cb(null, options.origin)
resolveOriginOption(req, (error, resolvedOriginOption) => {
if (error !== null) {
return next(error)
}
// Disable CORS and preflight if false
if (resolvedOriginOption === false) {
return next()
}
// Falsy values are invalid
if (!resolvedOriginOption) {
return next(new Error('Invalid CORS origin option'))
}
addCorsHeaders(req, reply, resolvedOriginOption, options)
if (req.raw.method === 'OPTIONS' && options.preflight === true) {
// Strict mode enforces the required headers for preflight
if (options.strictPreflight === true && (!req.headers.origin || !req.headers['access-control-request-method'])) {
reply.status(400).type('text/plain').send('Invalid Preflight Request')
return
}
req.corsPreflightEnabled = true
addPreflightHeaders(req, reply, options)
if (!options.preflightContinue) {
// Do not call the hook callback and terminate the request
// Safari (and potentially other browsers) need content-length 0,
// for 204 or they just hang waiting for a body
reply
.code(options.optionsSuccessStatus)
.header('Content-Length', '0')
.send()
return
}
}
return next()
})
}
function addCorsHeaders (req, reply, originOption, corsOptions) {
reply.header('Access-Control-Allow-Origin',
getAccessControlAllowOriginHeader(req.headers.origin, originOption))
if (corsOptions.credentials) {
reply.header('Access-Control-Allow-Credentials', 'true')
}
if (corsOptions.exposedHeaders !== null) {
reply.header(
'Access-Control-Expose-Headers',
Array.isArray(corsOptions.exposedHeaders) ? corsOptions.exposedHeaders.join(', ') : corsOptions.exposedHeaders
)
}
}
function addPreflightHeaders (req, reply, corsOptions) {
reply.header(
'Access-Control-Allow-Methods',
Array.isArray(corsOptions.methods) ? corsOptions.methods.join(', ') : corsOptions.methods
)
if (corsOptions.allowedHeaders === null) {
vary(reply, 'Access-Control-Request-Headers')
const reqAllowedHeaders = req.headers['access-control-request-headers']
if (reqAllowedHeaders !== undefined) {
reply.header('Access-Control-Allow-Headers', reqAllowedHeaders)
}
} else {
reply.header(
'Access-Control-Allow-Headers',
Array.isArray(corsOptions.allowedHeaders) ? corsOptions.allowedHeaders.join(', ') : corsOptions.allowedHeaders
)
}
if (corsOptions.maxAge !== null) {
reply.header('Access-Control-Max-Age', String(corsOptions.maxAge))
}
}
function resolveOriginWrapper (fastify, origin) {
return function (req, cb) {
const result = origin.call(fastify, req.headers.origin, cb)
// Allow for promises
if (result && typeof result.then === 'function') {
result.then(res => cb(null, res), cb)
}
}
}
function getAccessControlAllowOriginHeader (reqOrigin, originOption) {
if (originOption === '*') {
// allow any origin
return '*'
}
if (typeof originOption === 'string') {
// fixed origin
return originOption
}
// reflect origin
return isRequestOriginAllowed(reqOrigin, originOption) ? reqOrigin : false
}
function isRequestOriginAllowed (reqOrigin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (let i = 0; i < allowedOrigin.length; ++i) {
if (isRequestOriginAllowed(reqOrigin, allowedOrigin[i])) {
return true
}
}
return false
} else if (typeof allowedOrigin === 'string') {
return reqOrigin === allowedOrigin
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(reqOrigin)
} else {
return !!allowedOrigin
}
}
module.exports = fp(fastifyCors, {
fastify: '3.x',
name: 'fastify-cors'
})