├── .jshintrc ├── Makefile ├── .gitignore ├── examples ├── defaults-options.js ├── custom-options.js └── basic-auth-and-cors.js ├── package.json ├── LICENSE ├── README.md ├── index.js └── test └── index.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "node": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | PATH := node_modules/.bin:$(PATH) 3 | 4 | test: 5 | @mocha --harmony test 6 | 7 | .PHONY: test 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /examples/defaults-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default options example 3 | * 4 | */ 5 | 6 | var koa = require('koa'); 7 | var route = require('koa-route'); 8 | var cors = require('koa-cors'); 9 | var app = koa(); 10 | 11 | app.use(cors()); 12 | 13 | app.use(route.get('/', function() { 14 | this.body = { msg: 'Hello World!' }; 15 | })); 16 | 17 | app.listen(3000); 18 | -------------------------------------------------------------------------------- /examples/custom-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default options example 3 | * 4 | */ 5 | 6 | var koa = require('koa'); 7 | var route = require('koa-route'); 8 | var cors = require('koa-cors'); 9 | var app = koa(); 10 | 11 | var options = { 12 | origin: '*' 13 | }; 14 | 15 | app.use(cors(options)); 16 | 17 | app.use(route.get('/', function() { 18 | this.body = { msg: 'Hello World!' }; 19 | })); 20 | 21 | app.listen(3000); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-cors", 3 | "version": "0.0.16", 4 | "description": "CORS middleware for Koa", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --harmony test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/evert0n/koa-cors.git" 12 | }, 13 | "keywords": [ 14 | "cors", 15 | "koa", 16 | "koajs" 17 | ], 18 | "author": "Everton Yoshitani", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/evert0n/koa-cors/issues" 22 | }, 23 | "homepage": "https://github.com/evert0n/koa-cors", 24 | "devDependencies": { 25 | "koa": "^0.18.0", 26 | "chai": "^2.0.0", 27 | "mocha": "^2.1.0", 28 | "superagent": "^0.21.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/basic-auth-and-cors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic Auth and CORS 3 | * 4 | */ 5 | var auth = require('koa-basic-auth'); 6 | var cors = require('koa-cors'); 7 | var koa = require('koa'); 8 | var app = koa(); 9 | 10 | // cors 11 | app.use(cors()); 12 | 13 | // custom 401 handling 14 | app.use(function *(next){ 15 | try { 16 | yield next; 17 | } catch (err) { 18 | if (401 == err.status) { 19 | this.status = 401; 20 | this.set('WWW-Authenticate', 'Basic'); 21 | this.body = 'cant haz that'; 22 | } else { 23 | throw err; 24 | } 25 | } 26 | }); 27 | 28 | // require auth 29 | app.use(auth({ name: 'tj', pass: 'tobi' })); 30 | 31 | // secret response 32 | app.use(function *(){ 33 | this.body = 'secret'; 34 | }); 35 | 36 | app.listen(3000); 37 | console.log('listening on port 3000'); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Everton Yoshitani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | koa-cors 2 | ======== 3 | 4 | CORS middleware for Koa 5 | 6 | Inspired by the great [node-cors](https://github.com/troygoode/node-cors) module. 7 | 8 | ## Installation (via [npm](https://npmjs.org/package/koa-cors)) 9 | 10 | ```bash 11 | $ npm install koa-cors 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```javascript 17 | var koa = require('koa'); 18 | var route = require('koa-route'); 19 | var cors = require('koa-cors'); 20 | var app = koa(); 21 | 22 | app.use(cors()); 23 | 24 | app.use(route.get('/', function() { 25 | this.body = { msg: 'Hello World!' }; 26 | })); 27 | 28 | app.listen(3000); 29 | ``` 30 | 31 | ## Options 32 | 33 | ### origin 34 | 35 | Configures the **Access-Control-Allow-Origin** CORS header. Expects a string 36 | (ex: http://example.com). Set to `true` to reflect the 37 | [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), as defined 38 | by `req.header('Origin')`. Set to `false` to disable CORS. Can also be set to a 39 | function, which takes the request as the first parameter. 40 | 41 | ### expose 42 | 43 | Configures the **Access-Control-Expose-Headers** CORS header. Expects a 44 | comma-delimited string (ex: 'WWW-Authenticate,Server-Authorization') or an array 45 | (ex: `['WWW-Authenticate', 'Server-Authorization']`). Set this to pass the 46 | header, otherwise it is omitted. 47 | 48 | ### maxAge 49 | 50 | Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass 51 | the header, otherwise it is omitted. 52 | 53 | ### credentials 54 | 55 | Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` 56 | to pass the header, otherwise it is omitted. 57 | 58 | ### methods 59 | 60 | Configures the **Access-Control-Allow-Methods** CORS header. Expects a 61 | comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 62 | 'POST']`). 63 | 64 | ### headers 65 | Configures the **Access-Control-Allow-Headers** CORS header. Expects a 66 | comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: 67 | `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting 68 | the headers specified in the request's **Access-Control-Request-Headers** 69 | header. 70 | 71 | 72 | For details on the effect of each CORS header, 73 | [read this article on HTML5 Rocks](http://www.html5rocks.com/en/tutorials/cors/). 74 | 75 | 76 | ## License 77 | 78 | [MIT License](http://www.opensource.org/licenses/mit-license.php) 79 | 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * CORS middleware 5 | * 6 | * @param {Object} [options] 7 | * @return {GeneratorFunction} 8 | * @api public 9 | */ 10 | module.exports = function getMiddleware(options) { 11 | 12 | options = options || {}; 13 | 14 | var defaults = { 15 | origin: true, 16 | methods: 'GET,HEAD,PUT,POST,DELETE' 17 | }; 18 | 19 | // Set defaults 20 | for (var key in defaults) { 21 | if (!options.hasOwnProperty(key)) { 22 | options[key] = defaults[key]; 23 | } 24 | } 25 | 26 | // Set expose 27 | if (Array.isArray(options.expose)) { 28 | options.expose = options.expose.join(','); 29 | } 30 | 31 | // Set maxAge 32 | if (typeof options.maxAge === 'number') { 33 | options.maxAge = options.maxAge.toString(); 34 | } else { 35 | options.maxAge = null; 36 | } 37 | 38 | // Set methods 39 | if (Array.isArray(options.methods)) { 40 | options.methods = options.methods.join(','); 41 | } 42 | 43 | // Set headers 44 | if (Array.isArray(options.headers)) { 45 | options.headers = options.headers.join(','); 46 | } 47 | 48 | return function* cors(next) { 49 | 50 | /** 51 | * Access Control Allow Origin 52 | */ 53 | var origin; 54 | 55 | if (typeof options.origin === 'string') { 56 | origin = options.origin; 57 | } else if (options.origin === true) { 58 | origin = this.get('origin') || '*'; 59 | } else if (options.origin === false) { 60 | origin = options.origin; 61 | } else if (typeof options.origin === 'function') { 62 | origin = options.origin(this.request); 63 | } 64 | 65 | if (origin === false) { 66 | yield next; 67 | return ; 68 | } 69 | 70 | this.set('Access-Control-Allow-Origin', origin); 71 | 72 | /** 73 | * Access Control Expose Headers 74 | */ 75 | if (options.expose) { 76 | this.set('Access-Control-Expose-Headers', options.expose); 77 | } 78 | 79 | /** 80 | * Access Control Max Age 81 | */ 82 | if (options.maxAge) { 83 | this.set('Access-Control-Max-Age', options.maxAge); 84 | } 85 | 86 | /** 87 | * Access Control Allow Credentials 88 | */ 89 | if (options.credentials === true) { 90 | this.set('Access-Control-Allow-Credentials', 'true'); 91 | } 92 | 93 | /** 94 | * Access Control Allow Methods 95 | */ 96 | this.set('Access-Control-Allow-Methods', options.methods); 97 | 98 | /** 99 | * Access Control Allow Headers 100 | */ 101 | var headers; 102 | 103 | if (options.headers) { 104 | headers = options.headers; 105 | } else { 106 | headers = this.get('access-control-request-headers'); 107 | } 108 | 109 | if (headers) { 110 | this.set('Access-Control-Allow-Headers', headers); 111 | } 112 | 113 | /** 114 | * Returns 115 | */ 116 | if (this.method === 'OPTIONS') { 117 | this.status = 204; 118 | } else { 119 | yield next; 120 | } 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var koa = require('koa'); 2 | var http = require('http'); 3 | var chai = require('chai'); 4 | var cors = require('../'); 5 | var superagent = require('superagent'); 6 | 7 | var app, server; 8 | 9 | describe('cors()', function() { 10 | 11 | beforeEach(function() { 12 | setupServer(); 13 | }); 14 | 15 | it('should set "Access-Control-Allow-Origin" to "*"', function(done) { 16 | superagent.get('http://localhost:3000') 17 | .end(function(response) { 18 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); 19 | 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should set "Access-Control-Allow-Origin" to "example.org"', function(done) { 25 | superagent.get('http://localhost:3000') 26 | .set('Origin', 'example.org') 27 | .end(function(response) { 28 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('example.org'); 29 | 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should update "Access-Control-Allow-Origin" for each request', function(done) { 35 | superagent.get('http://localhost:3000') 36 | .end(function(response) { 37 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); 38 | 39 | superagent.get('http://localhost:3000') 40 | .set('Origin', 'localhost') 41 | .end(function(response) { 42 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('localhost'); 43 | 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | it('should not set "Access-Control-Expose-Headers"', function(done) { 50 | superagent.get('http://localhost:3000') 51 | .end(function(response) { 52 | chai.expect(response.get('Access-Control-Expose-Headers')).to.not.exist; 53 | 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should not set "Access-Control-Allow-Max-Age"', function(done) { 59 | superagent.get('http://localhost:3000') 60 | .end(function(response) { 61 | chai.expect(response.get('Access-Control-Allow-Max-Age')).to.not.exist; 62 | 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should not set "Access-Control-Allow-Methods"', function(done) { 68 | superagent.get('http://localhost:3000') 69 | .end(function(response) { 70 | chai.expect(response.get('Access-Control-Allow-Methods')).to.equal('GET,HEAD,PUT,POST,DELETE'); 71 | 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should not set "Access-Control-Allow-Credentials"', function(done) { 77 | superagent.get('http://localhost:3000') 78 | .end(function(response) { 79 | chai.expect(response.get('Access-Control-Allow-Credentials')).to.not.exist; 80 | 81 | done(); 82 | }); 83 | }); 84 | 85 | it('should set "Access-Control-Allow-Headers" to "Accept"', function(done) { 86 | superagent.get('http://localhost:3000') 87 | .set('Access-Control-Request-Headers', 'Accept') 88 | .end(function(response) { 89 | chai.expect(response.get('Access-Control-Allow-Headers')).to.equal('Accept'); 90 | 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should set "Access-Control-Allow-Headers" to "X-Foo"', function(done) { 96 | superagent.get('http://localhost:3000') 97 | .set('Access-Control-Request-Headers', 'X-Foo') 98 | .end(function(response) { 99 | chai.expect(response.get('Access-Control-Allow-Headers')).to.equal('X-Foo'); 100 | 101 | done(); 102 | }); 103 | }); 104 | 105 | it('should not fix value of "Access-Control-Allow-Headers"', function(done) { 106 | superagent.get('http://localhost:3000') 107 | .set('Access-Control-Request-Headers', 'X-Foo') 108 | .end(function() { 109 | superagent.get('http://localhost:3000') 110 | .set('Access-Control-Request-Headers', 'X-Bar') 111 | .end(function(response) { 112 | chai.expect(response.get('Access-Control-Allow-Headers')).to.equal('X-Bar'); 113 | 114 | done(); 115 | }); 116 | }); 117 | }); 118 | 119 | }); 120 | 121 | describe('cors({ origin: true })', function() { 122 | 123 | beforeEach(function() { 124 | setupServer({ origin: true }); 125 | }); 126 | 127 | it('should set "Access-Control-Allow-Origin" to "*"', function(done) { 128 | superagent.get('http://localhost:3000') 129 | .end(function(response) { 130 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); 131 | 132 | done(); 133 | }); 134 | }); 135 | 136 | it('should set "Access-Control-Allow-Origin" to "example.org"', function(done) { 137 | superagent.get('http://localhost:3000') 138 | .set('Origin', 'example.org') 139 | .end(function(response) { 140 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('example.org'); 141 | 142 | done(); 143 | }); 144 | }); 145 | 146 | }); 147 | 148 | describe('cors({ origin: false })', function() { 149 | 150 | beforeEach(function() { 151 | setupServer({ origin: false }); 152 | }); 153 | 154 | it('should not set any "Access-Control-Allow-*" header', function(done) { 155 | superagent.get('http://localhost:3000') 156 | .end(function(response) { 157 | chai.expect(response.get('Access-Control-Allow-Origin')).to.not.exist; 158 | chai.expect(response.get('Access-Control-Allow-Methods')).to.not.exist; 159 | 160 | done(); 161 | }); 162 | }); 163 | 164 | }); 165 | 166 | describe('cors({ origin: [function]})', function() { 167 | 168 | beforeEach(function() { 169 | var originWhiteList = ["localhost", "otherhost.com"]; 170 | 171 | var originFunction = function(req) { 172 | var origin = req.header.origin; 173 | if (originWhiteList.indexOf(origin) !== -1) { 174 | return origin; 175 | } 176 | return false; 177 | } 178 | 179 | setupServer({ origin: originFunction }); 180 | }); 181 | 182 | it('should not set any "Access-Control-Allow-*" header', function(done) { 183 | superagent.get('http://localhost:3000') 184 | .set('Origin', 'example.com') 185 | .end(function(response) { 186 | chai.expect(response.get('Access-Control-Allow-Origin')).to.not.exist; 187 | chai.expect(response.get('Access-Control-Allow-Methods')).to.not.exist; 188 | chai.expect(response.statusCode).to.equal(200); 189 | done(); 190 | }); 191 | }); 192 | 193 | it('should set "Access-Control-Allow-Origin" to "otherhost.com"', function(done) { 194 | superagent.get('http://localhost:3000') 195 | .set('Origin', 'otherhost.com') 196 | .end(function(response) { 197 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('otherhost.com'); 198 | 199 | done(); 200 | }); 201 | }); 202 | 203 | it('should set "Access-Control-Allow-Origin" to "localhost"', function(done) { 204 | superagent.get('http://localhost:3000') 205 | .set('Origin', 'localhost') 206 | .end(function(response) { 207 | chai.expect(response.get('Access-Control-Allow-Origin')).to.equal('localhost'); 208 | 209 | done(); 210 | }); 211 | }); 212 | 213 | }); 214 | 215 | describe('cors({ expose: "Acccept,Authorization" })', function() { 216 | 217 | beforeEach(function() { 218 | setupServer({ expose: 'Accept,Authorization' }); 219 | }); 220 | 221 | it('should set "Access-Control-Expose-Headers" header', function(done) { 222 | superagent.get('http://localhost:3000') 223 | .end(function(response) { 224 | chai.expect(response.get('Access-Control-Expose-Headers')) 225 | .to.equal('Accept,Authorization'); 226 | 227 | done(); 228 | }); 229 | }); 230 | 231 | }); 232 | 233 | describe('cors({ expose: ["Acccept", "Authorization"] })', function() { 234 | 235 | beforeEach(function() { 236 | setupServer({ expose: ['Accept', 'Authorization'] }); 237 | }); 238 | 239 | it('should set "Access-Control-Expose-Headers" header', function(done) { 240 | superagent.get('http://localhost:3000') 241 | .end(function(response) { 242 | chai.expect(response.get('Access-Control-Expose-Headers')) 243 | .to.equal('Accept,Authorization'); 244 | 245 | done(); 246 | }); 247 | }); 248 | 249 | }); 250 | 251 | describe('cors({ maxAge: 60 * 24 })', function() { 252 | 253 | beforeEach(function() { 254 | setupServer({ maxAge: 60 * 24 }); 255 | }); 256 | 257 | it('should set "Access-Control-Max-Age" header', function(done) { 258 | superagent.get('http://localhost:3000') 259 | .end(function(response) { 260 | chai.expect(response.get('Access-Control-Max-Age')).to.equal('1440'); 261 | 262 | done(); 263 | }); 264 | }); 265 | 266 | }); 267 | 268 | afterEach(function() { 269 | server.close(); 270 | }); 271 | 272 | function setupServer(options) { 273 | app = koa(); 274 | 275 | app.use(cors(options)); 276 | 277 | app.use(function *(next) { 278 | this.body = 'Hello'; 279 | }); 280 | 281 | server = http.createServer(app.callback()).listen(3000); 282 | } 283 | --------------------------------------------------------------------------------