UNPKG

@hapi/hapi

Version:

HTTP Server framework

209 lines (148 loc) 5.41 kB
'use strict'; const Boom = require('@hapi/boom'); const Hoek = require('@hapi/hoek'); let Route = null; // Delayed load due to circular dependency const internals = {}; exports.route = function (options) { if (!options) { return false; } const settings = Hoek.clone(options); settings._headers = settings.headers.concat(settings.additionalHeaders); settings._headersString = settings._headers.join(','); for (let i = 0; i < settings._headers.length; ++i) { settings._headers[i] = settings._headers[i].toLowerCase(); } if (settings._headers.indexOf('origin') === -1) { settings._headers.push('origin'); } settings._exposedHeaders = settings.exposedHeaders.concat(settings.additionalExposedHeaders).join(','); if (settings.origin === 'ignore') { settings._origin = false; } else if (settings.origin.indexOf('*') !== -1) { Hoek.assert(settings.origin.length === 1, 'Cannot specify cors.origin * together with other values'); settings._origin = true; } else { settings._origin = { qualified: [], wildcards: [] }; for (const origin of settings.origin) { if (origin.indexOf('*') !== -1) { settings._origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$')); } else { settings._origin.qualified.push(origin); } } } return settings; }; exports.options = function (route, server) { if (route.method === 'options' || !route.settings.cors) { return; } exports.handler(server); }; exports.handler = function (server) { Route = Route || require('./route'); if (server._core.router.specials.options) { return; } const definition = { method: '_special', path: '/{p*}', handler: internals.handler, options: { cors: false } }; const route = new Route(definition, server, { special: true }); server._core.router.special('options', route); }; internals.handler = function (request, h) { // Validate CORS preflight request const method = request.headers['access-control-request-method']; if (!method) { throw Boom.notFound('CORS error: Missing Access-Control-Request-Method header'); } // Lookup route const route = request.server.match(method, request.path, request.info.hostname); if (!route) { throw Boom.notFound(); } const settings = route.settings.cors; if (!settings) { return { message: 'CORS is disabled for this route' }; } // Validate Origin header const origin = request.headers.origin; if (!origin && settings._origin !== false) { throw Boom.notFound('CORS error: Missing Origin header'); } if (!exports.matchOrigin(origin, settings)) { return { message: 'CORS error: Origin not allowed' }; } // Validate allowed headers let headers = request.headers['access-control-request-headers']; if (headers) { headers = headers.toLowerCase().split(/\s*,\s*/); if (Hoek.intersect(headers, settings._headers).length !== headers.length) { return { message: 'CORS error: Some headers are not allowed' }; } } // Reply with the route CORS headers const response = h.response(); response.code(settings.preflightStatusCode); response._header('access-control-allow-origin', settings._origin ? origin : '*'); response._header('access-control-allow-methods', method); response._header('access-control-allow-headers', settings._headersString); response._header('access-control-max-age', settings.maxAge); if (settings.credentials) { response._header('access-control-allow-credentials', 'true'); } if (settings._exposedHeaders) { response._header('access-control-expose-headers', settings._exposedHeaders); } return response; }; exports.headers = function (response) { const request = response.request; const settings = request.route.settings.cors; if (settings._origin !== false) { response.vary('origin'); } if ((request.info.cors && !request.info.cors.isOriginMatch) || // After route lookup !exports.matchOrigin(request.headers.origin, request.route.settings.cors)) { // Response from onRequest return; } response._header('access-control-allow-origin', settings._origin ? request.headers.origin : '*'); if (settings.credentials) { response._header('access-control-allow-credentials', 'true'); } if (settings._exposedHeaders) { response._header('access-control-expose-headers', settings._exposedHeaders, { append: true }); } }; exports.matchOrigin = function (origin, settings) { if (settings._origin === true || settings._origin === false) { return true; } if (!origin) { return false; } if (settings._origin.qualified.indexOf(origin) !== -1) { return true; } for (const wildcard of settings._origin.wildcards) { if (origin.match(wildcard)) { return true; } } return false; };