├── .gitignore ├── .npmignore ├── test ├── mocha.opts ├── test.js ├── origin.spec.js ├── cors.actual.spec.js └── cors.preflight.spec.js ├── .travis.yml ├── src ├── actual.js ├── constants.js ├── origin-matcher.js ├── preflight.js └── index.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | .travis.yml 4 | test 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --slow 500 3 | --timeout 1000 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | - "4" 7 | - "stable" 8 | script: "npm test" 9 | cache: 10 | directories: 11 | - node_modules 12 | jobs: 13 | include: 14 | - stage: npm release 15 | script: echo "Deploying to npm ..." 16 | deploy: 17 | provider: npm 18 | email: npm@tabdigital.com.au 19 | api_key: $NPM_TOKEN 20 | on: 21 | tags: true 22 | branch: master 23 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var restify = require('restify') 2 | var cors = require('../src/index') 3 | 4 | exports.corsServer = function (corsConfig) { 5 | var middleware = cors(corsConfig) 6 | var server = restify.createServer() 7 | server.pre(middleware.preflight) 8 | server.use(middleware.actual) 9 | server.get('/test', function (req, res, next) { 10 | res.header('Custom-Response-Header', '123456') 11 | res.header('Max-Age', 5 * 60 * 1000) 12 | res.send(200, 'ok') 13 | next() 14 | }) 15 | return server 16 | } 17 | 18 | exports.noHeader = function (name) { 19 | return function (res) { 20 | if (res.headers.hasOwnProperty(name)) { 21 | return 'Should not have header ' + name 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/actual.js: -------------------------------------------------------------------------------- 1 | var originMatcher = require('./origin-matcher.js') 2 | var constants = require('./constants.js') 3 | 4 | exports.handler = function (options) { 5 | var matcher = originMatcher.create(options.origins) 6 | return function (req, res, next) { 7 | var originHeader = req.headers['origin'] 8 | 9 | // If either no origin was set, or the origin isn't supported, continue 10 | // without setting any headers 11 | if (!originHeader || !matcher(originHeader)) { 12 | return next() 13 | } 14 | 15 | // if match was found, let's set some headers. 16 | res.setHeader(constants['AC_ALLOW_ORIGIN'], originHeader) 17 | res.setHeader(constants['STR_VARY'], constants['STR_ORIGIN']) 18 | if (options.credentials) { 19 | res.setHeader(constants['AC_ALLOW_CREDS'], 'true') 20 | } 21 | res.setHeader(constants['AC_EXPOSE_HEADERS'], 22 | options.exposeHeaders.join(', ')) 23 | 24 | return next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ALLOW_HEADERS: [ 3 | 'accept', 4 | 'accept-version', 5 | 'content-type', 6 | 'request-id', 7 | 'origin', 8 | 'x-api-version', 9 | 'x-request-id', 10 | 'x-requested-with' 11 | ], 12 | EXPOSE_HEADERS: [ 13 | 'api-version', 14 | 'content-length', 15 | 'content-md5', 16 | 'content-type', 17 | 'date', 18 | 'request-id', 19 | 'response-time' 20 | ], 21 | AC_REQ_METHOD: 'access-control-request-method', 22 | AC_REQ_HEADERS: 'access-control-request-headers', 23 | AC_ALLOW_CREDS: 'access-control-allow-credentials', 24 | AC_ALLOW_ORIGIN: 'access-control-allow-origin', 25 | AC_ALLOW_HEADERS: 'access-control-allow-headers', 26 | AC_ALLOW_METHODS: 'access-control-allow-methods', 27 | AC_EXPOSE_HEADERS: 'access-control-expose-headers', 28 | AC_MAX_AGE: 'access-control-max-age', 29 | STR_VARY: 'vary', 30 | STR_ORIGIN: 'origin', 31 | HTTP_NO_CONTENT: 204 32 | } 33 | -------------------------------------------------------------------------------- /src/origin-matcher.js: -------------------------------------------------------------------------------- 1 | 2 | exports.create = function (allowedOrigins) { 3 | // pre-compile list of matchers, so regexes are only built once 4 | var matchers = allowedOrigins.map(createMatcher) 5 | // does a given request Origin match the list? 6 | return function (requestOrigin) { 7 | if (requestOrigin) { 8 | return matchers.some(matcher => matcher(requestOrigin)) 9 | } else { 10 | return false 11 | } 12 | } 13 | } 14 | 15 | function createMatcher (allowedOrigin) { 16 | if (allowedOrigin instanceof RegExp) { 17 | return requestOrigin => requestOrigin.match(allowedOrigin) 18 | } else if (allowedOrigin.indexOf('*') === -1) { 19 | // simple string comparison 20 | return requestOrigin => requestOrigin === allowedOrigin 21 | } else { 22 | // need to build a regex 23 | var regex = '^' + allowedOrigin.replace('.', '\\.').replace('*', '.*') + '$' 24 | return requestOrigin => requestOrigin.match(regex) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restify-cors-middleware", 3 | "version": "1.1.1", 4 | "author": "Tabcorp Digital Technology Team", 5 | "license": "MIT", 6 | "description": "CORS middleware with full W3C spec support", 7 | "keywords": [ 8 | "restify", 9 | "cors", 10 | "cross origin", 11 | "headers" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/TabDigital/restify-cors-middleware.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/TabDigital/restify-cors-middleware/issues" 19 | }, 20 | "main": "src/index.js", 21 | "scripts": { 22 | "test": "require-lint && standard && mocha" 23 | }, 24 | "peerDependencies": { 25 | "restify": "2.6.x - 7.x.x" 26 | }, 27 | "devDependencies": { 28 | "mocha": "~3.4.1", 29 | "require-lint": "^1.1.2", 30 | "restify": "~4.3.0", 31 | "should": "~11.2.1", 32 | "standard": "^10.0.2", 33 | "supertest": "~3.0.0" 34 | }, 35 | "dependencies": { 36 | "assert-plus": "^1.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/preflight.js: -------------------------------------------------------------------------------- 1 | var originMatcher = require('./origin-matcher') 2 | var constants = require('./constants.js') 3 | 4 | exports.handler = function (options) { 5 | var matcher = originMatcher.create(options.origins) 6 | return function (req, res, next) { 7 | if (req.method !== 'OPTIONS') return next() 8 | 9 | // 6.2.1 and 6.2.2 10 | var originHeader = req.headers['origin'] 11 | if (!matcher(originHeader)) return next() 12 | 13 | // 6.2.3 14 | var requestedMethod = req.headers[constants['AC_REQ_METHOD']] 15 | if (!requestedMethod) return next() 16 | 17 | // 6.2.4 18 | // var requestedHeaders = req.headers[constants['AC_REQ_HEADERS']] 19 | // requestedHeaders = requestedHeaders ? requestedHeaders.split(', ') : [] 20 | 21 | var allowedMethods = [requestedMethod, 'OPTIONS'] 22 | var allowedHeaders = options.allowHeaders 23 | 24 | res.once('header', function () { 25 | // 6.2.7 26 | res.header(constants['AC_ALLOW_ORIGIN'], originHeader) 27 | res.header(constants['AC_ALLOW_CREDS'], true) 28 | 29 | // 6.2.8 30 | if (options.preflightMaxAge) { 31 | res.header(constants['AC_MAX_AGE'], options.preflightMaxAge) 32 | } 33 | 34 | // 6.2.9 35 | res.header(constants['AC_ALLOW_METHODS'], allowedMethods.join(', ')) 36 | 37 | // 6.2.10 38 | res.header(constants['AC_ALLOW_HEADERS'], allowedHeaders.join(', ')) 39 | }) 40 | 41 | res.send(constants['HTTP_NO_CONTENT']) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/origin.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | require('should') 3 | var originMatcher = require('../src/origin-matcher') 4 | 5 | describe('Origin list', function () { 6 | it('returns false if the request has no origin', function () { 7 | var list = ['http://api.myapp.com', 'http://www.myapp.com'] 8 | var matcher = originMatcher.create(list) 9 | matcher(null).should.eql(false) 10 | matcher('').should.eql(false) 11 | }) 12 | 13 | it('returns false if the origin is not in the list', function () { 14 | var list = ['http://api.myapp.com', 'http://www.myapp.com'] 15 | var matcher = originMatcher.create(list) 16 | matcher('http://random-website.com').should.eql(false) 17 | }) 18 | 19 | it('returns true if the origin matched', function () { 20 | var list = ['http://api.myapp.com', 'http://www.myapp.com'] 21 | var matcher = originMatcher.create(list) 22 | matcher('http://api.myapp.com').should.eql(true) 23 | }) 24 | 25 | it('does not do partial matches by default', function () { 26 | var list = ['http://api.myapp.com', 'http://www.myapp.com'] 27 | var matcher = originMatcher.create(list) 28 | matcher('api.myapp.com').should.eql(false) 29 | }) 30 | 31 | it('always matches if the list contains *', function () { 32 | var list = ['*'] 33 | var matcher = originMatcher.create(list) 34 | matcher('http://random-website.com').should.eql(true) 35 | }) 36 | 37 | it('supports * for partial matches', function () { 38 | var list = ['http://*.myapp.com', 'http://other-website.com'] 39 | var matcher = originMatcher.create(list) 40 | matcher('http://api.myapp.com').should.eql(true) 41 | }) 42 | 43 | it('escapes the partial regex properly', function () { 44 | // the "." should be a real dot, not mean "[any character]myapp" 45 | var list = ['http://*.myapp.com', 'http://other-website.com'] 46 | var matcher = originMatcher.create(list) 47 | matcher('http://xmyapp.com').should.eql(false) 48 | }) 49 | 50 | it('returns false if there was no partial match', function () { 51 | var list = ['http://*.myapp.com'] 52 | var matcher = originMatcher.create(list) 53 | matcher('http://random-website.com').should.eql(false) 54 | }) 55 | 56 | it('supports regular expressions', function () { 57 | var list = ['http://api.myapp.com', /https?:\/\/example.com(:8888)?/] 58 | var matcher = originMatcher.create(list) 59 | matcher('https://example.com:8888').should.eql(true) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # restify-cors-middleware 2 | 3 | > CORS middleware with full [W3C spec](https://www.w3.org/TR/cors/) support. 4 | 5 | [![NPM](http://img.shields.io/npm/v/restify-cors-middleware.svg?style=flat)](https://npmjs.org/package/restify-cors-middleware) 6 | [![License](http://img.shields.io/npm/l/restify-cors-middleware.svg?style=flat)](https://github.com/TabDigital/restify-cors-middleware) 7 | 8 | [![Build Status](http://img.shields.io/travis/TabDigital/restify-cors-middleware.svg?style=flat)](http://travis-ci.org/TabDigital/restify-cors-middleware) 9 | [![Dependencies](http://img.shields.io/david/TabDigital/restify-cors-middleware.svg?style=flat)](https://david-dm.org/TabDigital/restify-cors-middleware) 10 | [![Dev dependencies](http://img.shields.io/david/dev/TabDigital/restify-cors-middleware.svg?style=flat)](https://david-dm.org/TabDigital/restify-cors-middleware) 11 | [![Peer dependencies](http://img.shields.io/david/peer/TabDigital/restify-cors-middleware.svg?style=flat)](https://david-dm.org/TabDigital/restify-cors-middleware) 12 | [![Known Vulnerabilities](https://snyk.io/package/npm/restify-cors-middleware/badge.svg)](https://snyk.io/package/npm/restify-cors-middleware) 13 | 14 | [![JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 15 | 16 | ## Setup 17 | ```sh 18 | $ npm install restify-cors-middleware --save 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | const corsMiddleware = require('restify-cors-middleware') 25 | 26 | const cors = corsMiddleware({ 27 | preflightMaxAge: 5, //Optional 28 | origins: ['http://api.myapp.com', 'http://web.myapp.com'], 29 | allowHeaders: ['API-Token'], 30 | exposeHeaders: ['API-Token-Expiry'] 31 | }) 32 | 33 | server.pre(cors.preflight) 34 | server.use(cors.actual) 35 | ``` 36 | 37 | ## Allowed origins 38 | 39 | You can specify the full list of domains and subdomains allowed in your application, using strings or regular expressions. 40 | 41 | ```js 42 | origins: [ 43 | 'http://myapp.com', 44 | 'http://*.myapp.com', 45 | /^https?:\/\/myapp.com(:[\d]+)?$/ 46 | ] 47 | ``` 48 | 49 | For added security, this middleware sets `Access-Control-Allow-Origin` to the origin that matched, not the configured wildcard. 50 | This means callers won't know about other domains that are supported. 51 | 52 | Setting `origins: ['*']` is also valid, although it comes with obvious security implications. Note that it will still return a customised response (matching Origin), so any caching layer (reverse proxy or CDN) will grow in size accordingly. 53 | 54 | ## Troubleshooting 55 | 56 | As per the spec, requests without an `Origin` will not receive any headers. Requests with a matching `Origin` will receive the appropriate response headers. Always be careful that any reverse proxies (e.g. Varnish) very their cache depending on the origin, so you don't serve CORS headers to the wrong request. 57 | 58 | ## Compliance to the spec 59 | 60 | See [unit tests](https://github.com/TabDigital/restify-cors-middleware/tree/master/test) for examples of preflight and actual requests. 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert-plus') 2 | var preflight = require('./preflight') 3 | var actual = require('./actual') 4 | var constants = require('./constants.js') 5 | 6 | /** 7 | * From http://www.w3.org/TR/cors/#resource-processing-model 8 | * 9 | * If "simple" request (paraphrased): 10 | * 11 | * 1. If the Origin header is not set, or if the value of Origin is not a 12 | * case-sensitive match to any values listed in `opts.origins`, do not 13 | * send any CORS headers 14 | * 15 | * 2. If the resource supports credentials add a single 16 | * 'Access-Control-Allow-Credentials' header with the value as "true", and 17 | * ensure 'AC-Allow-Origin' is not '*', but is the request header value, 18 | * otherwise add a single Access-Control-Allow-Origin header, with either the 19 | * value of the Origin header or the string "*" as value 20 | * 21 | * 3. Add Access-Control-Expose-Headers as appropriate 22 | * 23 | * @public 24 | * @function createCorsContext 25 | * @param {Object} options an options object 26 | * @param {Array} [options.origins] an array of whitelisted origins, can be 27 | * both strings and regular expressions 28 | * @param {Boolean} [options.credentials] if true, uses creds 29 | * @param {Array} [options.allowHeaders] user defined headers to allow 30 | * @param {Array} [options.exposeHeaders] user defined headers to expose 31 | * @param {Number} [options.preflightMaxAge] seconds to cache preflight requests 32 | * @param {Object | Function} [options.preflightStrategy] 33 | * customize preflight request handling 34 | * @returns {Object} returns an object with actual and preflight handlers 35 | */ 36 | module.exports = function (options) { 37 | assert.object(options, 'options') 38 | assert.optionalArray(options.origins, 'options.origins') 39 | if (options.origins) { 40 | options.origins.forEach(function (o) { 41 | assert.ok(typeof o === 'string' || o instanceof RegExp, o + 42 | ' is not a valid origin') 43 | }) 44 | } 45 | assert.optionalBool(options.credentials, 'options.credentials') 46 | assert.optionalArrayOfString(options.allowHeaders, 'options.allowHeaders') 47 | assert.optionalArrayOfString(options.exposeHeaders, 48 | 'options.exposeHeaders') 49 | assert.optionalNumber(options.preflightMaxAge, 'options.preflightMaxAge') 50 | assert.optionalObject(options.preflightStrategy, 51 | 'options.preflightStrategy') 52 | 53 | var opts = options 54 | opts.origins = options.origins || ['*'] 55 | opts.credentials = options.credentials || false 56 | opts.allowHeaders = options.allowHeaders || [] 57 | opts.exposeHeaders = options.exposeHeaders || [] 58 | 59 | assert.ok(options.origins.indexOf('*') === -1 || 60 | options.credentials === false, 61 | 'credentials not supported with wildcard') 62 | 63 | constants['EXPOSE_HEADERS'].forEach(function (h) { 64 | if (opts.exposeHeaders.indexOf(h) === -1) { 65 | opts.exposeHeaders.push(h) 66 | } 67 | }) 68 | 69 | constants['ALLOW_HEADERS'].forEach(function (h) { 70 | if (opts.allowHeaders.indexOf(h) === -1) { 71 | opts.allowHeaders.push(h) 72 | } 73 | }) 74 | 75 | return { 76 | actual: actual.handler(opts), 77 | preflight: preflight.handler(opts) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/cors.actual.spec.js: -------------------------------------------------------------------------------- 1 | // 2 | // Based on the spec at http://www.w3.org/TR/cors/ 3 | // The test numbers correspond to steps in the specification 4 | // 5 | /* eslint-env mocha */ 6 | 7 | var request = require('supertest') 8 | var should = require('should') 9 | var test = require('./test') 10 | 11 | describe('CORS: simple / actual requests', function () { 12 | it('6.1.1 Does not set headers if Origin is missing', function (done) { 13 | var server = test.corsServer({ 14 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 15 | }) 16 | request(server) 17 | .get('/test') 18 | .expect(test.noHeader('access-control-allow-origin')) 19 | .expect(200) 20 | .end(done) 21 | }) 22 | 23 | it('6.1.2 Does not set headers if Origin does not match', function (done) { 24 | var server = test.corsServer({ 25 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 26 | }) 27 | request(server) 28 | .get('/test') 29 | .set('Origin', 'http://random-website.com') 30 | .expect(test.noHeader('access-control-allow-origin')) 31 | .expect(200) 32 | .end(done) 33 | }) 34 | 35 | it('6.1.3 Sets Allow-Origin headers if the Origin matches', function (done) { 36 | var server = test.corsServer({ 37 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 38 | }) 39 | request(server) 40 | .get('/test') 41 | .set('Origin', 'http://api.myapp.com') 42 | .expect('access-control-allow-origin', 'http://api.myapp.com') 43 | .expect(200) 44 | .end(done) 45 | }) 46 | 47 | it('6.1.3 Does not set Access-Control-Allow-Credentials header if Origin is *', function (done) { 48 | should.throws(function () { 49 | test.corsServer({ 50 | origins: ['*'], 51 | credentials: true 52 | }) 53 | }) 54 | done() 55 | }) 56 | 57 | it('6.1.3 Sets Access-Control-Allow-Credentials header if configured', function (done) { 58 | var server = test.corsServer({ 59 | origins: ['http://api.myapp.com'], 60 | credentials: true 61 | }) 62 | request(server) 63 | .get('/test') 64 | .set('Origin', 'http://api.myapp.com') 65 | .expect('access-control-allow-credentials', 'true') 66 | .expect(200) 67 | .end(done) 68 | }) 69 | 70 | it('6.1.4 Does not set exposed headers if empty', function (done) { 71 | var server = test.corsServer({ 72 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 73 | }) 74 | request(server) 75 | .get('/test') 76 | .set('Origin', 'http://api.myapp.com') 77 | .expect('access-control-allow-origin', 'http://api.myapp.com') 78 | .expect('access-control-expose-headers', /api-version/) // defaults 79 | .expect(200) 80 | .end(done) 81 | }) 82 | 83 | it('6.1.4 Sets exposed headers if configured', function (done) { 84 | var server = test.corsServer({ 85 | origins: ['http://api.myapp.com', 'http://www.myapp.com'], 86 | exposeHeaders: ['HeaderA', 'HeaderB'] 87 | }) 88 | request(server) 89 | .get('/test') 90 | .set('Origin', 'http://api.myapp.com') 91 | .expect('access-control-allow-origin', 'http://api.myapp.com') 92 | .expect('access-control-expose-headers', /HeaderA, HeaderB/) // custom 93 | .expect('access-control-expose-headers', /api-version/) // defaults 94 | .expect(200) 95 | .end(done) 96 | }) 97 | 98 | it('Does not throw if "origins" option left undefined', function () { 99 | should.doesNotThrow(function createServer () { 100 | test.corsServer({}) 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/cors.preflight.spec.js: -------------------------------------------------------------------------------- 1 | // 2 | // Based on the spec at http://www.w3.org/TR/cors/ 3 | // The test numbers correspond to steps in the specification 4 | // 5 | /* eslint-env mocha */ 6 | 7 | var request = require('supertest') 8 | var test = require('./test') 9 | 10 | var METHOD_NOT_ALLOWED = 405 11 | 12 | describe('CORS: preflight requests', function () { 13 | it('6.2.1 Does not set headers if Origin is missing', function (done) { 14 | var server = test.corsServer({ 15 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 16 | }) 17 | request(server) 18 | .options('/test') 19 | .expect(test.noHeader('access-control-allow-origin')) 20 | .expect(METHOD_NOT_ALLOWED) 21 | .end(done) 22 | }) 23 | 24 | it('6.2.2 Does not set headers if Origin does not match', function (done) { 25 | var server = test.corsServer({ 26 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 27 | }) 28 | request(server) 29 | .options('/test') 30 | .set('Origin', 'http://random-website.com') 31 | .expect(test.noHeader('access-control-allow-origin')) 32 | .expect(METHOD_NOT_ALLOWED) 33 | .end(done) 34 | }) 35 | 36 | it('6.2.3 Does not set headers if Access-Control-Request-Method is missing', function (done) { 37 | var server = test.corsServer({ 38 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 39 | }) 40 | request(server) 41 | .options('/test') 42 | .set('Origin', 'http://api.myapp.com') 43 | .expect(test.noHeader('access-control-allow-origin')) 44 | .expect(test.noHeader('access-control-allow-methods')) 45 | .expect(METHOD_NOT_ALLOWED) 46 | .end(done) 47 | }) 48 | 49 | xit('6.2.4 Does not terminate if parsing of Access-Control-Request-Headers fails', function (done) { 50 | done() 51 | }) 52 | 53 | xit('6.2.5 Always matches Access-Control-Request-Method (spec says it is acceptable)', function (done) { 54 | done() 55 | }) 56 | 57 | it('6.2.6 Does not set headers if Access-Control-Request-Headers does not match', function (done) { 58 | var server = test.corsServer({ 59 | origins: ['http://api.myapp.com', 'http://www.myapp.com'], 60 | acceptHeaders: ['API-Token'] 61 | }) 62 | request(server) 63 | .options('/test') 64 | .set('Origin', 'http://api.myapp.com') 65 | .set('Access-Control-Request-Headers', 'Weird-Header') 66 | .expect(test.noHeader('access-control-allow-origin')) 67 | .expect(test.noHeader('access-control-allow-methods')) 68 | .expect(test.noHeader('access-control-allow-headers')) 69 | .expect(METHOD_NOT_ALLOWED) 70 | .end(done) 71 | }) 72 | 73 | it('6.2.7 Set the Allow-Origin header if it matches', function (done) { 74 | var server = test.corsServer({ 75 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 76 | }) 77 | request(server) 78 | .options('/test') 79 | .set('Origin', 'http://api.myapp.com') 80 | .set('Access-Control-Request-Method', 'GET') 81 | .expect('Access-Control-Allow-Origin', 'http://api.myapp.com') 82 | .expect(204) 83 | .end(done) 84 | }) 85 | 86 | it('6.2.8 Set the Access-Control-Max-Age header if a max age is provided', function (done) { 87 | var server = test.corsServer({ 88 | preflightMaxAge: 5, 89 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 90 | }) 91 | request(server) 92 | .options('/test') 93 | .set('Origin', 'http://api.myapp.com') 94 | .set('Access-Control-Request-Method', 'GET') 95 | .expect('Access-Control-Max-Age', '5') 96 | .expect(204) 97 | .end(done) 98 | }) 99 | 100 | it('6.2.9 Set the Allow-Method header', function (done) { 101 | var server = test.corsServer({ 102 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 103 | }) 104 | request(server) 105 | .options('/test') 106 | .set('Origin', 'http://api.myapp.com') 107 | .set('Access-Control-Request-Method', 'GET') 108 | .expect('Access-Control-Allow-Methods', 'GET, OPTIONS') 109 | .expect(204) 110 | .end(done) 111 | }) 112 | 113 | it('6.2.10 Set the Allow-Headers to all configured custom headers', function (done) { 114 | var server = test.corsServer({ 115 | origins: ['http://api.myapp.com', 'http://www.myapp.com'], 116 | allowHeaders: ['HeaderA'] 117 | }) 118 | request(server) 119 | .options('/test') 120 | .set('Origin', 'http://api.myapp.com') 121 | .set('Access-Control-Request-Method', 'GET') 122 | .expect('Access-Control-Allow-Headers', /accept-version/) // restify defaults 123 | .expect('Access-Control-Allow-Headers', /x-api-version/) // restify defaults 124 | .expect('Access-Control-Allow-Headers', /HeaderA/) // custom header 125 | .expect(204) 126 | .end(done) 127 | }) 128 | 129 | it('[Not in spec] The Allow-Headers should not contain duplicates', function (done) { 130 | var server = test.corsServer({ 131 | origins: ['http://api.myapp.com', 'http://www.myapp.com'] 132 | }) 133 | request(server) 134 | .options('/test') 135 | .set('Origin', 'http://api.myapp.com') 136 | .set('Access-Control-Request-Method', 'GET') 137 | .expect(204) 138 | .then(function (request) { 139 | var allowHeaders = request.headers['access-control-allow-headers'].split(', ') 140 | 141 | if (((new Set(allowHeaders)).size !== allowHeaders.length)) { 142 | return done(new Error('duplicate header detected')) 143 | } 144 | 145 | done(null) 146 | }) 147 | }) 148 | }) 149 | --------------------------------------------------------------------------------