UNPKG

veloze

Version:

A modern and fast express-like webserver for the web

189 lines (173 loc) 5.27 kB
import { vary } from '../response/index.js' import { isProdEnv } from '../utils/index.js' /** * @typedef { import('../types.js').Request } Request * @typedef { import('../types.js').Response } Response * @typedef { import('../types.js').HandlerCb } HandlerCb * * @typedef {(string|RegExp|((req: Request) => boolean))} Origin * * @typedef {object} CorsOptions * @property {boolean} [preflightContinue=false] if `true` next middleware is called instead of responding the request * @property {Origin|Origin[]} [origin=/^http:\/\/(localhost|127\.0\.0\.1)(:\d{2,5}|)$/] list of allowed origins * @property {string} [methods='GET,HEAD,PUT,PATCH,POST,DELETE'] comma separated list of allowed methods * @property {boolean} [credentials=false] if `true` allow requests to send cookies, authorization headers, or TLS client certificates * @property {string} [headers] list any number of allowed headers, separated by commas * @property {string} [exposeHeaders] list of comma-separated header names that clients are allowed to access from a response. * @property {number} [maxAge] caching max-age in seconds. Is set to 7200 (2h) for NODE_ENV !== development */ const DEFAULTS = { origin: /^https?:\/\/(localhost|127\.0\.0\.1)(:\d{2,5}|)$/, methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue: false, credentials: false, maxAge: isProdEnv ? 7200 : undefined } /** * @param {any} any * @returns {string} */ const type = (any) => toString.call(any).slice(8, -1) /** * @param {Request} req * @param {Origin|Origin[]} origin * @param {string} [reqOrigin] * @returns {boolean} */ const isOriginAllowed = (req, origin, reqOrigin) => { switch (type(origin)) { case 'String': return origin === reqOrigin case 'RegExp': // @ts-expect-error return origin.test(reqOrigin) case 'Function': // @ts-expect-error return !!origin(req) case 'Array': // @ts-expect-error for (let i = 0; i < origin.length; i += 1) { if (isOriginAllowed(req, origin[i], reqOrigin)) { return true } } return false default: return false } } /** * @param {Request} req * @param {Response} res * @param {Origin|Origin[]} origin */ const accessControlAllowOriginHeader = (req, res, origin) => { const reqOrigin = req.headers.origin if (!origin || origin === '*') { res.setHeader('access-control-allow-origin', '*') return } if (reqOrigin && isOriginAllowed(req, origin, reqOrigin)) { res.setHeader('access-control-allow-origin', reqOrigin) vary(res, 'origin') } } /** * @param {Request} req * @param {Response} res * @param {string} methods */ const accessControlAllowMethodsHeader = (req, res, methods) => { res.setHeader('access-control-allow-methods', methods) } /** * @param {Request} req * @param {Response} res * @param {boolean} credentials */ const accessControlAllowCredentialsHeader = (req, res, credentials) => { if (credentials === true) { res.setHeader('access-control-allow-credentials', 'true') } } /** * @param {Request} req * @param {Response} res * @param {string} [allowedHeaders] */ const accessControlAllowHeaders = (req, res, allowedHeaders) => { let allowed = allowedHeaders if (!allowedHeaders) { allowed = req.headers['access-control-request-headers'] if (allowed) { vary(res, 'access-control-request-headers') } } if (allowed) { res.setHeader('access-control-allow-headers', allowed) } } /** * @param {Request} req * @param {Response} res * @param {string} [exposeHeaders] */ const accessControlExposedHeaders = (req, res, exposeHeaders) => { if (exposeHeaders) { res.setHeader('access-control-expose-headers', exposeHeaders) } } /** * @param {Request} req * @param {Response} res * @param {number} [maxAge] */ const accessControlMaxAgeHeader = (req, res, maxAge) => { if (typeof maxAge === 'number') { res.setHeader('access-control-max-age', maxAge) } } /** * Middleware to handle [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) * preflight and CORS response headers for different origins. * * @param {CorsOptions} options * @returns {HandlerCb} */ export function cors(options = {}) { const { preflightContinue, origin, methods, credentials, headers, exposeHeaders, maxAge } = { ...DEFAULTS, ...options } return function corsMw(req, res, next) { if (req.method === 'OPTIONS') { accessControlAllowOriginHeader(req, res, origin) accessControlAllowMethodsHeader(req, res, methods) accessControlAllowCredentialsHeader(req, res, credentials) accessControlAllowHeaders(req, res, headers) accessControlExposedHeaders(req, res, exposeHeaders) accessControlMaxAgeHeader(req, res, maxAge) if (preflightContinue) { next() } else { // browsers want to see content-length=0 with status=204 res.statusCode = 204 res.setHeader('content-length', '0') res.end() } return } accessControlAllowOriginHeader(req, res, origin) accessControlAllowCredentialsHeader(req, res, credentials) accessControlExposedHeaders(req, res, exposeHeaders) next() } }