UNPKG

@balderdash/sails-edge

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

218 lines (169 loc) 8.09 kB
module.exports = function(sails) { /** * Module dependencies. */ var _ = require('lodash'); /** * Expose hook definition */ return { SECURITY_LEVEL_NORMAL: 0, SECURITY_LEVEL_HIGH: 1, SECURITY_LEVEL_VERYHIGH: 2, defaults: { cors: { origin: '*', credentials: true, methods: 'GET, POST, PUT, DELETE, OPTIONS, HEAD', headers: 'content-type', exposeHeaders: '', securityLevel: 0, } }, initialize: function(cb) { sails.on('router:before', function () { // If we're setting CORS on all routes by default, set up a universal route for it here. // CORS can still be turned off for specific routes by setting "cors:false" if (sails.config.cors.allRoutes === true) { sails.router.bind('/*', sendHeaders(), 'all', {_middlewareType: 'CORS HOOK: sendHeaders'}); } // Otherwise clear all the headers by default else { sails.router.bind('/*', clearHeaders, 'all', {_middlewareType: 'CORS HOOK: clearHeaders'}); } var optionsRouteConfigs = {}; // Loop through all configured routes, looking for CORS options _.each(sails.config.routes, function(config, route) { var routeInfo = sails.util.detectVerb(route); var path = routeInfo.original.toLowerCase(); var verb = routeInfo.verb.toLowerCase(); if (!_.isUndefined(config.cors)) { optionsRouteConfigs[path] = optionsRouteConfigs[path] || {"default": sails.config.cors}; // If cors is set to "true", and we're not doing all routes by default, set // the CORS headers for this route using the default origin if (config.cors === true) { // Use the default CORS config for this path on an OPTIONS request optionsRouteConfigs[path][verb || "default"] = sails.config.cors; if (!sails.config.cors.allRoutes) { sails.router.bind(route, sendHeaders(), null); } } // If cors is set to "false", clear the CORS headers for this route else if (config.cors === false) { // Clear headers on an OPTIONS request for this path optionsRouteConfigs[path][verb || "default"] = "clear"; sails.router.bind(route, clearHeaders, null, {_middlewareType: 'CORS HOOK: clearHeaders'}); return; } // Else if cors is set to a string, use that has the origin else if (typeof config.cors === "string") { optionsRouteConfigs[path][verb || "default"] = _.extend({origin: config.cors},{methods: verb}); sails.router.bind(route, sendHeaders({origin:config.cors}), null); } // Else if cors is an object, use that as the config else if (_.isPlainObject(config.cors)) { // If the route has a verb, it shouldn't have a "methods" CORS setting if (routeInfo.verb && config.cors.methods) { sails.log.warn("Ignoring 'methods' CORS setting for route "+route+" because it has a verb specified."); config.cors.methods = verb; } optionsRouteConfigs[path][verb || "default"] = config.cors; sails.router.bind(route, sendHeaders(config.cors), null); } // Otherwise throw a warning else { sails.log.warn("Invalid CORS settings for route "+route); } } }); _.each(optionsRouteConfigs, function(config, path) { sails.router.bind("options "+path, sendHeaders(config, true), null, {_middlewareType: 'CORS HOOK: preflight'}); }); // IF SECURITY_LEVEL > "normal"--don't process requests from disallowed origins // // We can't just rely on the browser implementing "access-control-allow-origin" correctly; // we need to make sure that if a request is made from an origin that isn't whitelisted, // that we don't end up processing that request. if (sails.config.cors.securityLevel > sails.hooks.cors.SECURITY_LEVEL_NORMAL) { sails.router.bind('/*', function(req, res, next) { // If it's a cross-origin request, and the access-control-allow-origin header doesn't match // the origin header, then we don't approve of this origin and shouldn't continue // with this request. if (!sails.util.isSameOrigin(req, sails.config.cors.securityLevel == sails.hooks.cors.SECURITY_LEVEL_VERYHIGH) && res.get('Access-Control-Allow-Origin') != req.headers.origin) { return res.forbidden(); } return next(); }, null, {_middlewareType: 'CORS HOOK: catchall'}); } }); cb(); } }; function sendHeaders(_routeCorsConfig, isOptionsRoute) { if (!_routeCorsConfig) { _routeCorsConfig = {}; } var _sendHeaders = function(req, res, next) { var routeCorsConfig; // If this is an options route handler, pull the config to use based on the method // that would be used in the follow-on request if (isOptionsRoute) { var method = (req.headers['access-control-request-method'] || '').toLowerCase() || "default"; routeCorsConfig = _routeCorsConfig[method]; if (routeCorsConfig == 'clear') { return clearHeaders(req, res, next); } } // Otherwise just use the config that was passed down else { routeCorsConfig = _routeCorsConfig; } // If we have an origin header... if (req.headers && req.headers.origin) { // Get the allowed origins var origins = (routeCorsConfig.origin || sails.config.cors.origin).split(','); // Match the origin of the request against the allowed origins var foundOrigin = false; _.every(origins, function(origin) { origin = origin.trim(); // If we find a whitelisted origin, send the Access-Control-Allow-Origin header // to greenlight the request. if (origin == req.headers.origin || origin == "*") { res.set('Access-Control-Allow-Origin', req.headers.origin); foundOrigin = true; return false; } return true; }); if (!foundOrigin) { // For HTTP requests, set the Access-Control-Allow-Origin header to '', which the browser will // interpret as, "no way Jose." res.set('Access-Control-Allow-Origin', ''); } // Determine whether or not to allow cookies to be passed cross-origin res.set('Access-Control-Allow-Credentials', !_.isUndefined(routeCorsConfig.credentials) ? routeCorsConfig.credentials : sails.config.cors.credentials); // This header lets a server whitelist headers that browsers are allowed to access res.set('Access-Control-Expose-Headers', !_.isUndefined(routeCorsConfig.exposeHeaders) ? routeCorsConfig.exposeHeaders : sails.config.cors.exposeHeaders); // Handle preflight requests if (req.method == "OPTIONS") { res.set('Access-Control-Allow-Methods', !_.isUndefined(routeCorsConfig.methods) ? routeCorsConfig.methods : sails.config.cors.methods); res.set('Access-Control-Allow-Headers', !_.isUndefined(routeCorsConfig.headers) ? routeCorsConfig.headers : sails.config.cors.headers); } } next(); }; _sendHeaders._middlewareType = "CORS HOOK: sendHeaders"; return _sendHeaders; } function clearHeaders(req, res, next) { // If we can set headers (i.e. it's not a socket request), do so. if (res.set) { res.set('Access-Control-Allow-Origin', ''); res.set('Access-Control-Allow-Credentials', ''); res.set('Access-Control-Allow-Methods', ''); res.set('Access-Control-Allow-Headers', ''); res.set('Access-Control-Expose-Headers', ''); } next(); } };