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
			| 
											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' | ||
|  | }) |