This commit is contained in:
retrohacker 2017-05-09 13:23:44 -07:00
parent 1d12597dd6
commit 7aad8040ca
8 changed files with 164 additions and 73 deletions

View file

@ -23,8 +23,11 @@
},
"devDependencies": {
"mocha": "~3.2.0",
"should": "~11.2.1",
"restify": "~4.3.0",
"should": "~11.2.1",
"supertest": "~3.0.0"
},
"dependencies": {
"assert-plus": "^1.0.0"
}
}

View file

@ -1,20 +1,27 @@
var restify = require('restify');
//
// For now, delegate to restify.CORS
//
// Although there's a few things we could fix
// around Access-Control-Expose-Headers
//
// It would also break the cyclic dependency with Restify :)
//
var origin = require('./origin.js');
var constants = require('./constants.js');
exports.handler = function(options) {
return restify.CORS({
credentials: options.credentials,
origins: options.origins,
headers: options.exposeHeaders
});
return function(req, res, next) {
var originHeader = req.headers['origin'];
// If either no origin was set, or the origin isn't supported, continue
// without setting any headers
if (!originHeader || !origin.match(originHeader, options.origins)) {
return next();
}
// if match was found, let's set some headers.
res.setHeader(constants['AC_ALLOW_ORIGIN'], originHeader);
res.setHeader(constants['STR_VARY'], constants['STR_ORIGIN']);
if(options.credentials) {
res.setHeader(constants['AC_ALLOW_CREDS'], 'true');
}
res.setHeader(constants['AC_EXPOSE_HEADERS'],
options.exposeHeaders.join(', '));
return next();
};
};

32
src/constants.js Normal file
View file

@ -0,0 +1,32 @@
module.exports = {
ALLOW_HEADERS: [
'accept',
'accept-version',
'content-type',
'request-id',
'origin',
'x-api-version',
'x-request-id',
'x-requested-with'
],
EXPOSE_HEADERS: [
'api-version',
'content-length',
'content-md5',
'content-type',
'date',
'request-id',
'response-time'
],
AC_REQ_METHOD: 'access-control-request-method',
AC_REQ_HEADERS: 'access-control-request-headers',
AC_ALLOW_CREDS: 'access-control-allow-credentials',
AC_ALLOW_ORIGIN: 'access-control-allow-origin',
AC_ALLOW_HEADERS: 'access-control-allow-headers',
AC_ALLOW_METHODS: 'access-control-allow-methods',
AC_EXPOSE_HEADERS: 'access-control-expose-headers',
AC_MAX_AGE: 'access-control-max-age',
STR_VARY: 'vary',
STR_ORIGIN: 'origin',
HTTP_NO_CONTENT: 204
}

View file

@ -1,17 +1,79 @@
var util = require('util');
var assert = require('assert-plus');
var preflight = require('./preflight');
var actual = require('./actual');
var constants = require('./constants.js');
/**
* From http://www.w3.org/TR/cors/#resource-processing-model
*
* If "simple" request (paraphrased):
*
* 1. If the Origin header is not set, or if the value of Origin is not a
* case-sensitive match to any values listed in `opts.origins`, do not
* send any CORS headers
*
* 2. If the resource supports credentials add a single
* 'Access-Control-Allow-Credentials' header with the value as "true", and
* ensure 'AC-Allow-Origin' is not '*', but is the request header value,
* otherwise add a single Access-Control-Allow-Origin header, with either the
* value of the Origin header or the string "*" as value
*
* 3. Add Access-Control-Expose-Headers as appropriate
*
* @public
* @function createCorsContext
* @param {Object} options an options object
* @param {Array} [options.origins] an array of whitelisted origins, can be
* both strings and regular expressions
* @param {Boolean} [options.credentials] if true, uses creds
* @param {Array} [options.allowHeaders] user defined headers to allow
* @param {Array} [options.exposeHeaders] user defined headers to expose
* @param {Number} [options.preflightMaxAge] ms to cache preflight requests
* @param {Object | Function} [options.preflightStrategy]
* customize preflight request handling
* @returns {Object} returns an object with actual and preflight handlers
*/
module.exports = function(options) {
if (! util.isArray(options.origins)) options.origins = ['*'];
if (! util.isArray(options.allowHeaders)) options.allowHeaders = [];
if (! util.isArray(options.exposeHeaders)) options.exposeHeaders = [];
if (options.origins[0] === '*') options.credentials = false;
assert.object(options, 'options');
assert.optionalArray(options.origins, 'options.origins');
options.origins.forEach(function (o) {
assert.ok(typeof o === 'string' || o instanceof RegExp, o +
' is not a valid origin');
});
assert.optionalBool(options.credentials, 'options.credentials');
assert.optionalArrayOfString(options.allowHeaders, 'options.allowHeaders');
assert.optionalArrayOfString(options.exposeHeaders,
'options.exposeHeaders');
assert.optionalNumber(options.preflightMaxAge, 'options.preflightMaxAge');
assert.optionalObject(options.preflightStrategy,
'options.preflightStrategy');
var opts = options;
opts.origins = options.origins || ['*']
opts.credentials = options.credentials || false;
opts.allowHeaders = options.allowHeaders || [];
opts.exposeHeaders = options.exposeHeaders || [];
assert.ok(options.origins.indexOf('*') === -1 ||
options.credentials === false,
'credentials not supported with wildcard');
constants['EXPOSE_HEADERS'].forEach(function (h) {
if (opts.exposeHeaders.indexOf(h) === -1) {
opts.exposeHeaders.push(h);
}
});
constants['ALLOW_HEADERS'].forEach(function (h) {
if (opts.allowHeaders.indexOf(h) === -1) {
opts.allowHeaders.push(h);
}
});
return {
actual: actual.handler(options),
preflight: preflight.handler(options)
actual: actual.handler(opts),
preflight: preflight.handler(opts)
};
};

View file

@ -1,11 +1,16 @@
exports.match = function(incomingOrigin, origins) {
if(!incomingOrigin) {
return null;
}
exports.match = function(origin, list) {
function belongs(o) {
return (origin === o || o === "*");
for(var i = 0; i < origins.length; i++) {
var origin = origins[i];
if( (origin instanceof RegExp && origin.test(incomingOrigin)) ||
(typeof origin === 'string' && origin === incomingOrigin) ||
(origin === '*')) {
return incomingOrigin;
}
if (origin && list.some(belongs)) {
return origin;
} else {
return false;
}
return null;
};

View file

@ -1,19 +1,5 @@
var restify = require('restify');
var origin = require('./origin');
//
// For now we use the "default headers" from restify.CORS
// Maybe this should just be a global setting on this module
// (ie. list of extra Access-Control-Expose-Headers, regardless of what the middleware config says)
//
//
// TODO:
// Handle the spec better around "simple methods" and "simple headers".
//
var DEFAULT_ALLOW_HEADERS = restify.CORS.ALLOW_HEADERS;
var HTTP_NO_CONTENT = 204;
var constants = require('./constants.js');
exports.handler = function(options) {
@ -25,37 +11,36 @@ exports.handler = function(options) {
if (origin.match(originHeader, options.origins) === false) return next();
// 6.2.3
var requestedMethod = req.headers['access-control-request-method'];
var requestedMethod = req.headers[constants['AC_REQ_METHOD']];
if (!requestedMethod) return next();
// 6.2.4
var requestedHeaders = req.headers['access-control-request-headers'];
var requestedHeaders = req.headers[constants['AC_REQ_HEADERS']];
requestedHeaders = requestedHeaders ? requestedHeaders.split(', ') : [];
var allowedMethods = [requestedMethod, 'OPTIONS'];
var allowedHeaders = DEFAULT_ALLOW_HEADERS.concat(['x-requested-with'])
var allowedHeaders = constants['ALLOW_HEADERS']
.concat(options.allowHeaders);
res.once('header', function() {
// 6.2.7
res.header('Access-Control-Allow-Origin', originHeader);
res.header('Access-Control-Allow-Credentials', true);
res.header(constants['AC_ALLOW_ORIGIN'], originHeader);
res.header(constants['AC_ALLOW_CREDS'], true);
// 6.2.8
if (options.preflightMaxAge) {
res.header('Access-Control-Max-Age', options.preflightMaxAge);
res.header(constants['AC_MAX_AGE'], options.preflightMaxAge);
}
// 6.2.9
res.header('Access-Control-Allow-Methods', allowedMethods.join(', '));
res.header(constants['AC_ALLOW_METHODS'], allowedMethods.join(', '));
// 6.2.10
res.header('Access-Control-Allow-Headers', allowedHeaders.join(', '));
res.header(constants['AC_ALLOW_HEADERS'], allowedHeaders.join(', '));
});
res.send(HTTP_NO_CONTENT);
res.send(constants['HTTP_NO_CONTENT']);
};
};

View file

@ -45,16 +45,13 @@ describe('CORS: simple / actual requests', function() {
});
it('6.1.3 Does not set Access-Control-Allow-Credentials header if Origin is *', function(done) {
should.throws(function() {
var server = test.corsServer({
origins: ['*'],
credentials: true
});
request(server)
.get('/test')
.set('Origin', 'http://api.myapp.com')
.expect(test.noHeader('access-control-allow-credentials'))
.expect(200)
.end(done);
});
done();
});
it('6.1.3 Sets Access-Control-Allow-Credentials header if configured', function(done) {

View file

@ -10,12 +10,12 @@ describe('Origin list', function() {
it('returns null if the origin is not in the list', function() {
var o = origin.match('http://random-website.com', list);
o.should.eql(false);
(o === null).should.eql(true);
});
it('does not do partial matches', function() {
var o = origin.match('api.myapp.com', list);
o.should.eql(false);
(o === null).should.eql(true);
});
it('returns the origin if it matched', function() {