sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
283 lines (256 loc) • 8.88 kB
JavaScript
/**
* This script is a modified version of the Express 4 CORS module:
* https://github.com/expressjs/cors
* We're making a modified version because that one leaks headers,
* but is otherwise still more full-featured and well-thought-out
* than what we were previously using to set headers.
*
* By 'leaks headers', we mean that in certain cases the module
* would set headers like `Access-Control-Allow-Origin` or
* `Access-Control-Allow-Methods` even if the requesting origin
* was not whitelisted. User agents would still reject the response,
* but it would allow attackers to sniff some information about
* what the server _would_ allow.
*
* This version of the module _only_ sends headers if the origin
* in the request is whitelisted.
*/
(function () {
'use strict';
var vary = require('vary');
var defaults = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
// Note -- the original module default for this setting is 204
optionsSuccessStatus: 200
};
function isString(s) {
return typeof s === 'string' || s instanceof String;
}
function isOriginAllowed(origin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (var i = 0; i < allowedOrigin.length; ++i) {
if (isOriginAllowed(origin, allowedOrigin[i])) {
return true;
}
}
return false;
} else if (isString(allowedOrigin)) {
return origin === allowedOrigin;
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
} else {
return !!allowedOrigin;
}
}
function configureOrigin(options, req) {
var requestOrigin = req.headers.origin;
var headers = [];
// If the allowed origin is '*' (or not set, in which case defaulting to '*'),
// then we'll send an `Access-Control-Allow-Origin` header.
if (!options.origin || options.origin === '*') {
// allow any origin
headers.push([{
key: 'Access-Control-Allow-Origin',
value: '*'
}]);
}
// Otherwise we'll send the header if and ONLY if the requesting origin matches
// one of the whitelisted origins. Note that the Express CORS module makes an
// exception here if `origin` is set to a string, and always returns the header
// even if the requesting origin doesn't match, which seems like a security leak.
else {
if (isOriginAllowed(requestOrigin, options.origin)) {
// reflect origin
headers.push([{
key: 'Access-Control-Allow-Origin',
value: requestOrigin
}]);
// Also send a "vary" header to allow proxies to cache this request correctly.
headers.push([{
key: 'Vary',
value: 'Origin'
}]);
}
}
return headers;
}
function configureMethods(options) {
var methods = options.methods || defaults.methods;
if (methods.join) {
methods = options.methods.join(','); // .methods is an array, so turn it into a string
}
return {
key: 'Access-Control-Allow-Methods',
value: methods
};
}
function configureCredentials(options) {
if (options.credentials === true) {
return {
key: 'Access-Control-Allow-Credentials',
value: 'true'
};
}
return null;
}
function configureAllowedHeaders(options, req) {
var headers = options.allowedHeaders || options.headers;
if (!headers) {
headers = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
} else if (headers.join) {
headers = headers.join(','); // .headers is an array, so turn it into a string
}
if (headers && headers.length) {
return {
key: 'Access-Control-Allow-Headers',
value: headers
};
}
return null;
}
function configureExposedHeaders(options) {
var headers = options.exposedHeaders;
if (!headers) {
return null;
} else if (headers.join) {
headers = headers.join(','); // .headers is an array, so turn it into a string
}
if (headers && headers.length) {
return {
key: 'Access-Control-Expose-Headers',
value: headers
};
}
return null;
}
function configureMaxAge(options) {
var maxAge = options.maxAge && options.maxAge.toString();
if (maxAge && maxAge.length) {
return {
key: 'Access-Control-Max-Age',
value: maxAge
};
}
return null;
}
function applyHeaders(headers, res) {
for (var i = 0, n = headers.length; i < n; i++) {
var header = headers[i];
if (header) {
if (Array.isArray(header)) {
applyHeaders(header, res);
} else if (header.key === 'Vary' && header.value) {
vary(res, header.value);
} else if (header.value) {
res.setHeader(header.key, header.value);
}
}
}
}
function cors(options, req, res, next) {
var headers = [];
var method = req.method && req.method.toUpperCase && req.method.toUpperCase();
if (method === 'OPTIONS') {
// preflight
headers = configureOrigin(options, req);
// ONLY send additional headers if configureOrigin added the `Access-Control-Allow-Origin`
// header, meaning that the requesting origin was whitelisted.
if (headers.length) {
headers.push(configureCredentials(options, req));
headers.push(configureMethods(options, req));
headers.push(configureAllowedHeaders(options, req));
headers.push(configureMaxAge(options, req));
headers.push(configureExposedHeaders(options, req));
applyHeaders(headers, res);
}
if (options.preflightContinue ) {
return next();
} else {
res.statusCode = options.optionsSuccessStatus || defaults.optionsSuccessStatus;
res.end();
}
} else {
// actual response
headers = configureOrigin(options, req);
// ONLY send additional headers if configureOrigin added the `Access-Control-Allow-Origin`
// header, meaning that the requesting origin was whitelisted.
if (headers.length) {
headers.push(configureCredentials(options, req));
headers.push(configureExposedHeaders(options, req));
applyHeaders(headers, res);
}
return next();
}
}
function middlewareWrapper(o) {
// if no options were passed in, use the defaults
if (!o || o === true) {
o = {};
}
if (o.origin === undefined) {
o.origin = defaults.origin;
}
if (o.methods === undefined) {
o.methods = defaults.methods;
}
if (o.preflightContinue === undefined) {
o.preflightContinue = defaults.preflightContinue;
}
// if options are static (either via defaults or custom options passed in), wrap in a function
var optionsCallback = null;
if (typeof o === 'function') {
optionsCallback = o;
} else {
optionsCallback = function (req, cb) {
cb(null, o);
};
}
return function corsMiddleware(req, res, next) {
optionsCallback(req, function (err, options) {
// Transform the Sails CORS options configs into those expected by this module.
options = {
origin: options.allowOrigins,
credentials: options.allowCredentials,
methods: options.allowRequestMethods,
headers: options.allowRequestHeaders,
exposedHeaders: options.allowResponseHeaders
};
// If origin is `*` and `credentials` is true, that means that `allowAnyOriginWithCredentialsUnsafe`
// has been set in Sails, so we'll change the origin to `true` (which causes the request origin to
// be reflected in the response).
if (options.origin === '*' && options.credentials === true) {
options.origin = true;
}
if (err) {
return next(err);
} else {
var originCallback = null;
if (options.origin && typeof options.origin === 'function') {
originCallback = options.origin;
} else if (options.origin) {
originCallback = function (origin, cb) {
cb(null, options.origin);
};
}
if (originCallback) {
originCallback(req.headers.origin, function (err2, origin) {
if (err2 || !origin) {
return next(err2);
} else {
var corsOptions = Object.create(options);
corsOptions.origin = origin;
cors(corsOptions, req, res, next);
}
});
} else {
return next();
}
}
});
};
}
// can pass either an options hash, an options delegate, or nothing
module.exports = middlewareWrapper;
}());