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
						
					
					
				
			
		
		
	
	
							218 lines
						
					
					
						
							6.4 KiB
						
					
					
				| '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'
 | |
| })
 |