mirror of
https://github.com/scratchfoundation/restify-cors-middleware.git
synced 2024-12-18 03:43:09 -05:00
Merged with https://github.com/restify/plugins/pull/10
This commit is contained in:
parent
1d12597dd6
commit
7aad8040ca
8 changed files with 164 additions and 73 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
32
src/constants.js
Normal 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
|
||||
}
|
76
src/index.js
76
src/index.js
|
@ -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)
|
||||
};
|
||||
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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']);
|
||||
};
|
||||
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue