├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json └── test ├── .eslintrc.yml ├── error-response.js ├── example-app.js ├── issue-2.js ├── support └── env.js └── test.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | env: 3 | node: true 4 | rules: 5 | indent: ["error", 2, { "SwitchCase": 1 }] 6 | quotes: ["error", "single"] 7 | space-in-parens: ["error", "never"] 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | name: 13 | - Node.js 0.10 14 | - Node.js 4.x 15 | - Node.js 6.x 16 | - Node.js 8.x 17 | - Node.js 10.x 18 | - Node.js 12.x 19 | - Node.js 14.x 20 | - Node.js 16.x 21 | - Node.js 17.x 22 | - Node.js 18.x 23 | - Node.js 19.x 24 | - Node.js 20.x 25 | - Node.js 21.x 26 | - Node.js 22.x 27 | 28 | include: 29 | - name: Node.js 0.10 30 | node-version: "0.10" 31 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 32 | 33 | - name: Node.js 4.x 34 | node-version: "4.9" 35 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 36 | 37 | - name: Node.js 6.x 38 | node-version: "6.17" 39 | npm-i: mocha@6.2.2 nyc@14.1.1 40 | 41 | - name: Node.js 8.x 42 | node-version: "8.17" 43 | npm-i: mocha@7.2.0 nyc@14.1.1 44 | 45 | - name: Node.js 10.x 46 | node-version: "10.24" 47 | npm-i: mocha@8.3.2 48 | 49 | - name: Node.js 12.x 50 | node-version: "12.22" 51 | 52 | - name: Node.js 14.x 53 | node-version: "14.17" 54 | 55 | - name: Node.js 16.x 56 | node-version: "16.6" 57 | 58 | - name: Node.js 17.x 59 | node-version: "17.6" 60 | 61 | - name: Node.js 18.x 62 | node-version: "18.14" 63 | 64 | - name: Node.js 19.x 65 | node-version: "19.6" 66 | 67 | - name: Node.js 20.x 68 | node-version: "20.12" 69 | 70 | - name: Node.js 21.x 71 | node-version: "21.7" 72 | 73 | - name: Node.js 22.x 74 | node-version: "22.0" 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Install Node.js ${{ matrix.node-version }} 80 | shell: bash -eo pipefail -l {0} 81 | run: | 82 | nvm install --default ${{ matrix.node-version }} 83 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 84 | 85 | - name: Configure npm 86 | run: | 87 | if [[ "$(npm config get package-lock)" == "true" ]]; then 88 | npm config set package-lock false 89 | else 90 | npm config set shrinkwrap false 91 | fi 92 | 93 | - name: Install npm module(s) ${{ matrix.npm-i }} 94 | run: npm install --save-dev ${{ matrix.npm-i }} 95 | if: matrix.npm-i != '' 96 | 97 | - name: Setup Node.js version-specific dependencies 98 | shell: bash 99 | run: | 100 | # eslint for linting 101 | # - remove on Node.js < 10 102 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 103 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 104 | grep -E '^eslint(-|$)' | \ 105 | sort -r | \ 106 | xargs -n1 npm rm --silent --save-dev 107 | fi 108 | 109 | - name: Install Node.js dependencies 110 | run: npm install 111 | 112 | - name: List environment 113 | id: list_env 114 | shell: bash 115 | run: | 116 | echo "node@$(node -v)" 117 | echo "npm@$(npm -v)" 118 | npm -s ls ||: 119 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' 120 | 121 | - name: Run tests 122 | shell: bash 123 | run: | 124 | if npm -ps ls nyc | grep -q nyc; then 125 | npm run test-ci 126 | else 127 | npm test 128 | fi 129 | 130 | - name: Lint code 131 | if: steps.list_env.outputs.eslint != '' 132 | run: npm run lint 133 | 134 | - name: Collect code coverage 135 | uses: coverallsapp/github-action@master 136 | if: steps.list_env.outputs.nyc != '' 137 | with: 138 | github-token: ${{ secrets.GITHUB_TOKEN }} 139 | flag-name: run-${{ matrix.test_number }} 140 | parallel: true 141 | 142 | coverage: 143 | needs: test 144 | runs-on: ubuntu-latest 145 | steps: 146 | - name: Upload code coverage 147 | uses: coverallsapp/github-action@master 148 | with: 149 | github-token: ${{ secrets.github_token }} 150 | parallel-finished: true 151 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 42 | with: 43 | languages: javascript 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | 48 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 49 | # If this step fails, then you should remove it and run the build manually (see below) 50 | # - name: Autobuild 51 | # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 55 | 56 | # If the Autobuild fails above, remove it and uncomment the following three lines. 57 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 58 | 59 | # - run: | 60 | # echo "Run, Build Application using script" 61 | # ./location_of_script_within_repo/buildscript.sh 62 | 63 | - name: Perform CodeQL Analysis 64 | uses: github/codeql-action/analyze@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 65 | with: 66 | category: "/language:javascript" -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 72 | with: 73 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contributing to `cors` 2 | 3 | CORS is a node.js package for providing a [connect](http://www.senchalabs.org/connect/)/[express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options. Learn more about the project in [the README](README.md). 4 | 5 | ## The CORS Spec 6 | 7 | [http://www.w3.org/TR/cors/](http://www.w3.org/TR/cors/) 8 | 9 | ## Pull Requests Welcome 10 | 11 | * Include `'use strict';` in every javascript file. 12 | * 2 space indentation. 13 | * Please run the testing steps below before submitting. 14 | 15 | ## Testing 16 | 17 | ```bash 18 | $ npm install 19 | $ npm test 20 | ``` 21 | 22 | ## Interactive Testing Harness 23 | 24 | [http://node-cors-client.herokuapp.com](http://node-cors-client.herokuapp.com) 25 | 26 | Related git repositories: 27 | 28 | * [https://github.com/TroyGoode/node-cors-server](https://github.com/TroyGoode/node-cors-server) 29 | * [https://github.com/TroyGoode/node-cors-client](https://github.com/TroyGoode/node-cors-client) 30 | 31 | ## License 32 | 33 | [MIT License](http://www.opensource.org/licenses/mit-license.php) 34 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2.8.5 / 2018-11-04 2 | ================== 3 | 4 | * Fix setting `maxAge` option to `0` 5 | 6 | 2.8.4 / 2017-07-12 7 | ================== 8 | 9 | * Work-around Safari bug in default pre-flight response 10 | 11 | 2.8.3 / 2017-03-29 12 | ================== 13 | 14 | * Fix error when options delegate missing `methods` option 15 | 16 | 2.8.2 / 2017-03-28 17 | ================== 18 | 19 | * Fix error when frozen options are passed 20 | * Send "Vary: Origin" when using regular expressions 21 | * Send "Vary: Access-Control-Request-Headers" when dynamic `allowedHeaders` 22 | 23 | 2.8.1 / 2016-09-08 24 | ================== 25 | 26 | This release only changed documentation. 27 | 28 | 2.8.0 / 2016-08-23 29 | ================== 30 | 31 | * Add `optionsSuccessStatus` option 32 | 33 | 2.7.2 / 2016-08-23 34 | ================== 35 | 36 | * Fix error when Node.js running in strict mode 37 | 38 | 2.7.1 / 2015-05-28 39 | ================== 40 | 41 | * Move module into expressjs organization 42 | 43 | 2.7.0 / 2015-05-28 44 | ================== 45 | 46 | * Allow array of matching condition as `origin` option 47 | * Allow regular expression as `origin` option 48 | 49 | 2.6.1 / 2015-05-28 50 | ================== 51 | 52 | * Update `license` in package.json 53 | 54 | 2.6.0 / 2015-04-27 55 | ================== 56 | 57 | * Add `preflightContinue` option 58 | * Fix "Vary: Origin" header added for "*" 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Troy Goode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cors 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Build Status][github-actions-ci-image]][github-actions-ci-url] 6 | [![Test Coverage][coveralls-image]][coveralls-url] 7 | 8 | CORS is a node.js package for providing a [Connect](http://www.senchalabs.org/connect/)/[Express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options. 9 | 10 | **[Follow me (@troygoode) on Twitter!](https://twitter.com/intent/user?screen_name=troygoode)** 11 | 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Simple Usage](#simple-usage-enable-all-cors-requests) 15 | * [Enable CORS for a Single Route](#enable-cors-for-a-single-route) 16 | * [Configuring CORS](#configuring-cors) 17 | * [Configuring CORS w/ Dynamic Origin](#configuring-cors-w-dynamic-origin) 18 | * [Enabling CORS Pre-Flight](#enabling-cors-pre-flight) 19 | * [Configuring CORS Asynchronously](#configuring-cors-asynchronously) 20 | * [Configuration Options](#configuration-options) 21 | * [License](#license) 22 | * [Author](#author) 23 | 24 | ## Installation 25 | 26 | This is a [Node.js](https://nodejs.org/en/) module available through the 27 | [npm registry](https://www.npmjs.com/). Installation is done using the 28 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 29 | 30 | ```sh 31 | $ npm install cors 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Simple Usage (Enable *All* CORS Requests) 37 | 38 | ```javascript 39 | var express = require('express') 40 | var cors = require('cors') 41 | var app = express() 42 | 43 | app.use(cors()) 44 | 45 | app.get('/products/:id', function (req, res, next) { 46 | res.json({msg: 'This is CORS-enabled for all origins!'}) 47 | }) 48 | 49 | app.listen(80, function () { 50 | console.log('CORS-enabled web server listening on port 80') 51 | }) 52 | ``` 53 | 54 | ### Enable CORS for a Single Route 55 | 56 | ```javascript 57 | var express = require('express') 58 | var cors = require('cors') 59 | var app = express() 60 | 61 | app.get('/products/:id', cors(), function (req, res, next) { 62 | res.json({msg: 'This is CORS-enabled for a Single Route'}) 63 | }) 64 | 65 | app.listen(80, function () { 66 | console.log('CORS-enabled web server listening on port 80') 67 | }) 68 | ``` 69 | 70 | ### Configuring CORS 71 | 72 | ```javascript 73 | var express = require('express') 74 | var cors = require('cors') 75 | var app = express() 76 | 77 | var corsOptions = { 78 | origin: 'http://example.com', 79 | optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 80 | } 81 | 82 | app.get('/products/:id', cors(corsOptions), function (req, res, next) { 83 | res.json({msg: 'This is CORS-enabled for only example.com.'}) 84 | }) 85 | 86 | app.listen(80, function () { 87 | console.log('CORS-enabled web server listening on port 80') 88 | }) 89 | ``` 90 | 91 | ### Configuring CORS w/ Dynamic Origin 92 | 93 | This module supports validating the origin dynamically using a function provided 94 | to the `origin` option. This function will be passed a string that is the origin 95 | (or `undefined` if the request has no origin), and a `callback` with the signature 96 | `callback(error, origin)`. 97 | 98 | The `origin` argument to the callback can be any value allowed for the `origin` 99 | option of the middleware, except a function. See the 100 | [configuration options](#configuration-options) section for more information on all 101 | the possible value types. 102 | 103 | This function is designed to allow the dynamic loading of allowed origin(s) from 104 | a backing datasource, like a database. 105 | 106 | ```javascript 107 | var express = require('express') 108 | var cors = require('cors') 109 | var app = express() 110 | 111 | var corsOptions = { 112 | origin: function (origin, callback) { 113 | // db.loadOrigins is an example call to load 114 | // a list of origins from a backing database 115 | db.loadOrigins(function (error, origins) { 116 | callback(error, origins) 117 | }) 118 | } 119 | } 120 | 121 | app.get('/products/:id', cors(corsOptions), function (req, res, next) { 122 | res.json({msg: 'This is CORS-enabled for an allowed domain.'}) 123 | }) 124 | 125 | app.listen(80, function () { 126 | console.log('CORS-enabled web server listening on port 80') 127 | }) 128 | ``` 129 | 130 | ### Enabling CORS Pre-Flight 131 | 132 | Certain CORS requests are considered 'complex' and require an initial 133 | `OPTIONS` request (called the "pre-flight request"). An example of a 134 | 'complex' CORS request is one that uses an HTTP verb other than 135 | GET/HEAD/POST (such as DELETE) or that uses custom headers. To enable 136 | pre-flighting, you must add a new OPTIONS handler for the route you want 137 | to support: 138 | 139 | ```javascript 140 | var express = require('express') 141 | var cors = require('cors') 142 | var app = express() 143 | 144 | app.options('/products/:id', cors()) // enable pre-flight request for DELETE request 145 | app.del('/products/:id', cors(), function (req, res, next) { 146 | res.json({msg: 'This is CORS-enabled for all origins!'}) 147 | }) 148 | 149 | app.listen(80, function () { 150 | console.log('CORS-enabled web server listening on port 80') 151 | }) 152 | ``` 153 | 154 | You can also enable pre-flight across-the-board like so: 155 | 156 | ```javascript 157 | app.options('*', cors()) // include before other routes 158 | ``` 159 | 160 | NOTE: When using this middleware as an application level middleware (for 161 | example, `app.use(cors())`), pre-flight requests are already handled for all 162 | routes. 163 | 164 | ### Configuring CORS Asynchronously 165 | 166 | ```javascript 167 | var express = require('express') 168 | var cors = require('cors') 169 | var app = express() 170 | 171 | var allowlist = ['http://example1.com', 'http://example2.com'] 172 | var corsOptionsDelegate = function (req, callback) { 173 | var corsOptions; 174 | if (allowlist.indexOf(req.header('Origin')) !== -1) { 175 | corsOptions = { origin: true } // reflect (enable) the requested origin in the CORS response 176 | } else { 177 | corsOptions = { origin: false } // disable CORS for this request 178 | } 179 | callback(null, corsOptions) // callback expects two parameters: error and options 180 | } 181 | 182 | app.get('/products/:id', cors(corsOptionsDelegate), function (req, res, next) { 183 | res.json({msg: 'This is CORS-enabled for an allowed domain.'}) 184 | }) 185 | 186 | app.listen(80, function () { 187 | console.log('CORS-enabled web server listening on port 80') 188 | }) 189 | ``` 190 | 191 | ## Configuration Options 192 | 193 | * `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Possible values: 194 | - `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), as defined by `req.header('Origin')`, or set it to `false` to disable CORS. 195 | - `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. 196 | - `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com". 197 | - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com". 198 | - `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as `callback(err, origin)`, where `origin` is a non-function value of the `origin` option) as the second. 199 | * `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`). 200 | * `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header. 201 | * `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed. 202 | * `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted. 203 | * `maxAge`: Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted. 204 | * `preflightContinue`: Pass the CORS preflight response to the next handler. 205 | * `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`. 206 | 207 | The default configuration is the equivalent of: 208 | 209 | ```json 210 | { 211 | "origin": "*", 212 | "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", 213 | "preflightContinue": false, 214 | "optionsSuccessStatus": 204 215 | } 216 | ``` 217 | 218 | For details on the effect of each CORS header, read [this](https://web.dev/cross-origin-resource-sharing/) article on web.dev. 219 | 220 | ## License 221 | 222 | [MIT License](http://www.opensource.org/licenses/mit-license.php) 223 | 224 | ## Author 225 | 226 | [Troy Goode](https://github.com/TroyGoode) ([troygoode@gmail.com](mailto:troygoode@gmail.com)) 227 | 228 | [coveralls-image]: https://img.shields.io/coveralls/expressjs/cors/master.svg 229 | [coveralls-url]: https://coveralls.io/r/expressjs/cors?branch=master 230 | [downloads-image]: https://img.shields.io/npm/dm/cors.svg 231 | [downloads-url]: https://npmjs.org/package/cors 232 | [github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/cors/ci.yml?branch=master&label=ci 233 | [github-actions-ci-url]: https://github.com/expressjs/cors?query=workflow%3Aci 234 | [npm-image]: https://img.shields.io/npm/v/cors.svg 235 | [npm-url]: https://npmjs.org/package/cors 236 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var assign = require('object-assign'); 6 | var vary = require('vary'); 7 | 8 | var defaults = { 9 | origin: '*', 10 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 11 | preflightContinue: false, 12 | optionsSuccessStatus: 204 13 | }; 14 | 15 | function isString(s) { 16 | return typeof s === 'string' || s instanceof String; 17 | } 18 | 19 | function isOriginAllowed(origin, allowedOrigin) { 20 | if (Array.isArray(allowedOrigin)) { 21 | for (var i = 0; i < allowedOrigin.length; ++i) { 22 | if (isOriginAllowed(origin, allowedOrigin[i])) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } else if (isString(allowedOrigin)) { 28 | return origin === allowedOrigin; 29 | } else if (allowedOrigin instanceof RegExp) { 30 | return allowedOrigin.test(origin); 31 | } else { 32 | return !!allowedOrigin; 33 | } 34 | } 35 | 36 | function configureOrigin(options, req) { 37 | var requestOrigin = req.headers.origin, 38 | headers = [], 39 | isAllowed; 40 | 41 | if (!options.origin || options.origin === '*') { 42 | // allow any origin 43 | headers.push([{ 44 | key: 'Access-Control-Allow-Origin', 45 | value: '*' 46 | }]); 47 | } else if (isString(options.origin)) { 48 | // fixed origin 49 | headers.push([{ 50 | key: 'Access-Control-Allow-Origin', 51 | value: options.origin 52 | }]); 53 | headers.push([{ 54 | key: 'Vary', 55 | value: 'Origin' 56 | }]); 57 | } else { 58 | isAllowed = isOriginAllowed(requestOrigin, options.origin); 59 | // reflect origin 60 | headers.push([{ 61 | key: 'Access-Control-Allow-Origin', 62 | value: isAllowed ? requestOrigin : false 63 | }]); 64 | headers.push([{ 65 | key: 'Vary', 66 | value: 'Origin' 67 | }]); 68 | } 69 | 70 | return headers; 71 | } 72 | 73 | function configureMethods(options) { 74 | var methods = options.methods; 75 | if (methods.join) { 76 | methods = options.methods.join(','); // .methods is an array, so turn it into a string 77 | } 78 | return { 79 | key: 'Access-Control-Allow-Methods', 80 | value: methods 81 | }; 82 | } 83 | 84 | function configureCredentials(options) { 85 | if (options.credentials === true) { 86 | return { 87 | key: 'Access-Control-Allow-Credentials', 88 | value: 'true' 89 | }; 90 | } 91 | return null; 92 | } 93 | 94 | function configureAllowedHeaders(options, req) { 95 | var allowedHeaders = options.allowedHeaders || options.headers; 96 | var headers = []; 97 | 98 | if (!allowedHeaders) { 99 | allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers 100 | headers.push([{ 101 | key: 'Vary', 102 | value: 'Access-Control-Request-Headers' 103 | }]); 104 | } else if (allowedHeaders.join) { 105 | allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string 106 | } 107 | if (allowedHeaders && allowedHeaders.length) { 108 | headers.push([{ 109 | key: 'Access-Control-Allow-Headers', 110 | value: allowedHeaders 111 | }]); 112 | } 113 | 114 | return headers; 115 | } 116 | 117 | function configureExposedHeaders(options) { 118 | var headers = options.exposedHeaders; 119 | if (!headers) { 120 | return null; 121 | } else if (headers.join) { 122 | headers = headers.join(','); // .headers is an array, so turn it into a string 123 | } 124 | if (headers && headers.length) { 125 | return { 126 | key: 'Access-Control-Expose-Headers', 127 | value: headers 128 | }; 129 | } 130 | return null; 131 | } 132 | 133 | function configureMaxAge(options) { 134 | var maxAge = (typeof options.maxAge === 'number' || options.maxAge) && options.maxAge.toString() 135 | if (maxAge && maxAge.length) { 136 | return { 137 | key: 'Access-Control-Max-Age', 138 | value: maxAge 139 | }; 140 | } 141 | return null; 142 | } 143 | 144 | function applyHeaders(headers, res) { 145 | for (var i = 0, n = headers.length; i < n; i++) { 146 | var header = headers[i]; 147 | if (header) { 148 | if (Array.isArray(header)) { 149 | applyHeaders(header, res); 150 | } else if (header.key === 'Vary' && header.value) { 151 | vary(res, header.value); 152 | } else if (header.value) { 153 | res.setHeader(header.key, header.value); 154 | } 155 | } 156 | } 157 | } 158 | 159 | function cors(options, req, res, next) { 160 | var headers = [], 161 | method = req.method && req.method.toUpperCase && req.method.toUpperCase(); 162 | 163 | if (method === 'OPTIONS') { 164 | // preflight 165 | headers.push(configureOrigin(options, req)); 166 | headers.push(configureCredentials(options)) 167 | headers.push(configureMethods(options)) 168 | headers.push(configureAllowedHeaders(options, req)); 169 | headers.push(configureMaxAge(options)) 170 | headers.push(configureExposedHeaders(options)) 171 | applyHeaders(headers, res); 172 | 173 | if (options.preflightContinue) { 174 | next(); 175 | } else { 176 | // Safari (and potentially other browsers) need content-length 0, 177 | // for 204 or they just hang waiting for a body 178 | res.statusCode = options.optionsSuccessStatus; 179 | res.setHeader('Content-Length', '0'); 180 | res.end(); 181 | } 182 | } else { 183 | // actual response 184 | headers.push(configureOrigin(options, req)); 185 | headers.push(configureCredentials(options)) 186 | headers.push(configureExposedHeaders(options)) 187 | applyHeaders(headers, res); 188 | next(); 189 | } 190 | } 191 | 192 | function middlewareWrapper(o) { 193 | // if options are static (either via defaults or custom options passed in), wrap in a function 194 | var optionsCallback = null; 195 | if (typeof o === 'function') { 196 | optionsCallback = o; 197 | } else { 198 | optionsCallback = function (req, cb) { 199 | cb(null, o); 200 | }; 201 | } 202 | 203 | return function corsMiddleware(req, res, next) { 204 | optionsCallback(req, function (err, options) { 205 | if (err) { 206 | next(err); 207 | } else { 208 | var corsOptions = assign({}, defaults, options); 209 | var originCallback = null; 210 | if (corsOptions.origin && typeof corsOptions.origin === 'function') { 211 | originCallback = corsOptions.origin; 212 | } else if (corsOptions.origin) { 213 | originCallback = function (origin, cb) { 214 | cb(null, corsOptions.origin); 215 | }; 216 | } 217 | 218 | if (originCallback) { 219 | originCallback(req.headers.origin, function (err2, origin) { 220 | if (err2 || !origin) { 221 | next(err2); 222 | } else { 223 | corsOptions.origin = origin; 224 | cors(corsOptions, req, res, next); 225 | } 226 | }); 227 | } else { 228 | next(); 229 | } 230 | } 231 | }); 232 | }; 233 | } 234 | 235 | // can pass either an options hash, an options delegate, or nothing 236 | module.exports = middlewareWrapper; 237 | 238 | }()); 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cors", 3 | "description": "Node.js CORS middleware", 4 | "version": "2.8.5", 5 | "author": "Troy Goode (https://github.com/troygoode/)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "cors", 9 | "express", 10 | "connect", 11 | "middleware" 12 | ], 13 | "repository": "expressjs/cors", 14 | "main": "./lib/index.js", 15 | "dependencies": { 16 | "object-assign": "^4", 17 | "vary": "^1" 18 | }, 19 | "devDependencies": { 20 | "after": "0.8.2", 21 | "eslint": "7.30.0", 22 | "express": "4.17.1", 23 | "mocha": "9.1.1", 24 | "nyc": "15.1.0", 25 | "supertest": "6.1.3" 26 | }, 27 | "files": [ 28 | "lib/index.js", 29 | "CONTRIBUTING.md", 30 | "HISTORY.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "engines": { 35 | "node": ">= 0.10" 36 | }, 37 | "scripts": { 38 | "test": "npm run lint && npm run test-ci", 39 | "test-ci": "nyc --reporter=lcov --reporter=text mocha --require test/support/env", 40 | "lint": "eslint lib test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/error-response.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var express = require('express'), 6 | supertest = require('supertest'), 7 | cors = require('../lib'); 8 | 9 | var app; 10 | 11 | /* -------------------------------------------------------------------------- */ 12 | 13 | app = express(); 14 | app.use(cors()); 15 | 16 | app.post('/five-hundred', function (req, res, next) { 17 | next(new Error('nope')); 18 | }); 19 | 20 | app.post('/four-oh-one', function (req, res, next) { 21 | next(new Error('401')); 22 | }); 23 | 24 | app.post('/four-oh-four', function (req, res, next) { 25 | next(); 26 | }); 27 | 28 | app.use(function (err, req, res, next) { 29 | if (err.message === '401') { 30 | res.status(401).send('unauthorized'); 31 | } else { 32 | next(err); 33 | } 34 | }); 35 | 36 | /* -------------------------------------------------------------------------- */ 37 | 38 | describe('error response', function () { 39 | it('500', function (done) { 40 | supertest(app) 41 | .post('/five-hundred') 42 | .expect(500) 43 | .expect('Access-Control-Allow-Origin', '*') 44 | .expect(/Error: nope/) 45 | .end(done) 46 | }); 47 | 48 | it('401', function (done) { 49 | supertest(app) 50 | .post('/four-oh-one') 51 | .expect(401) 52 | .expect('Access-Control-Allow-Origin', '*') 53 | .expect('unauthorized') 54 | .end(done) 55 | }); 56 | 57 | it('404', function (done) { 58 | supertest(app) 59 | .post('/four-oh-four') 60 | .expect(404) 61 | .expect('Access-Control-Allow-Origin', '*') 62 | .end(done) 63 | }); 64 | }); 65 | 66 | }()); 67 | -------------------------------------------------------------------------------- /test/example-app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var express = require('express'), 6 | supertest = require('supertest'), 7 | cors = require('../lib'); 8 | 9 | var simpleApp, 10 | complexApp; 11 | 12 | /* -------------------------------------------------------------------------- */ 13 | 14 | simpleApp = express(); 15 | simpleApp.head('/', cors(), function (req, res) { 16 | res.status(204).send(); 17 | }); 18 | simpleApp.get('/', cors(), function (req, res) { 19 | res.send('Hello World (Get)'); 20 | }); 21 | simpleApp.post('/', cors(), function (req, res) { 22 | res.send('Hello World (Post)'); 23 | }); 24 | 25 | /* -------------------------------------------------------------------------- */ 26 | 27 | complexApp = express(); 28 | complexApp.options('/', cors()); 29 | complexApp.delete('/', cors(), function (req, res) { 30 | res.send('Hello World (Delete)'); 31 | }); 32 | 33 | /* -------------------------------------------------------------------------- */ 34 | 35 | describe('example app(s)', function () { 36 | describe('simple methods', function () { 37 | it('GET works', function (done) { 38 | supertest(simpleApp) 39 | .get('/') 40 | .expect(200) 41 | .expect('Access-Control-Allow-Origin', '*') 42 | .expect('Hello World (Get)') 43 | .end(done) 44 | }); 45 | it('HEAD works', function (done) { 46 | supertest(simpleApp) 47 | .head('/') 48 | .expect(204) 49 | .expect('Access-Control-Allow-Origin', '*') 50 | .end(done) 51 | }); 52 | it('POST works', function (done) { 53 | supertest(simpleApp) 54 | .post('/') 55 | .expect(200) 56 | .expect('Access-Control-Allow-Origin', '*') 57 | .expect('Hello World (Post)') 58 | .end(done) 59 | }); 60 | }); 61 | 62 | describe('complex methods', function () { 63 | it('OPTIONS works', function (done) { 64 | supertest(complexApp) 65 | .options('/') 66 | .expect(204) 67 | .expect('Access-Control-Allow-Origin', '*') 68 | .end(done) 69 | }); 70 | it('DELETE works', function (done) { 71 | supertest(complexApp) 72 | .del('/') 73 | .expect(200) 74 | .expect('Access-Control-Allow-Origin', '*') 75 | .expect('Hello World (Delete)') 76 | .end(done) 77 | }); 78 | }); 79 | }); 80 | 81 | }()); 82 | -------------------------------------------------------------------------------- /test/issue-2.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | var express = require('express'), 6 | supertest = require('supertest'), 7 | cors = require('../lib'); 8 | 9 | var app, 10 | corsOptions; 11 | 12 | /* -------------------------------------------------------------------------- */ 13 | 14 | app = express(); 15 | corsOptions = { 16 | origin: true, 17 | methods: ['POST'], 18 | credentials: true, 19 | maxAge: 3600 20 | }; 21 | app.options('/api/login', cors(corsOptions)); 22 | app.post('/api/login', cors(corsOptions), function (req, res) { 23 | res.send('LOGIN'); 24 | }); 25 | 26 | /* -------------------------------------------------------------------------- */ 27 | 28 | describe('issue #2', function () { 29 | it('OPTIONS works', function (done) { 30 | supertest(app) 31 | .options('/api/login') 32 | .set('Origin', 'http://example.com') 33 | .expect(204) 34 | .expect('Access-Control-Allow-Origin', 'http://example.com') 35 | .end(done) 36 | }); 37 | it('POST works', function (done) { 38 | supertest(app) 39 | .post('/api/login') 40 | .set('Origin', 'http://example.com') 41 | .expect(200) 42 | .expect('Access-Control-Allow-Origin', 'http://example.com') 43 | .expect('LOGIN') 44 | .end(done) 45 | }); 46 | }); 47 | 48 | }()); 49 | -------------------------------------------------------------------------------- /test/support/env.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'test'; 3 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var EventEmitter = require('events').EventEmitter 4 | var util = require('util') 5 | 6 | ;(function () { 7 | 'use strict'; 8 | 9 | var after = require('after') 10 | var assert = require('assert') 11 | var cors = require('..') 12 | 13 | var fakeRequest = function (method, headers) { 14 | return new FakeRequest(method, headers) 15 | } 16 | 17 | var fakeResponse = function () { 18 | return new FakeResponse() 19 | } 20 | 21 | describe('cors', function () { 22 | it('does not alter `options` configuration object', function () { 23 | var options = Object.freeze({ 24 | origin: 'custom-origin' 25 | }); 26 | assert.doesNotThrow(function () { 27 | cors(options); 28 | }) 29 | }); 30 | 31 | it('passes control to next middleware', function (done) { 32 | // arrange 33 | var req, res, next; 34 | req = fakeRequest('GET'); 35 | res = fakeResponse(); 36 | next = function () { 37 | done(); 38 | }; 39 | 40 | // act 41 | cors()(req, res, next); 42 | }); 43 | 44 | it('shortcircuits preflight requests', function (done) { 45 | var cb = after(1, done) 46 | var req = new FakeRequest('OPTIONS') 47 | var res = new FakeResponse() 48 | 49 | res.on('finish', function () { 50 | assert.equal(res.statusCode, 204) 51 | cb() 52 | }) 53 | 54 | cors()(req, res, function (err) { 55 | cb(err || new Error('should not be called')) 56 | }) 57 | }); 58 | 59 | it('can configure preflight success response status code', function (done) { 60 | var cb = after(1, done) 61 | var req = new FakeRequest('OPTIONS') 62 | var res = new FakeResponse() 63 | 64 | res.on('finish', function () { 65 | assert.equal(res.statusCode, 200) 66 | cb() 67 | }) 68 | 69 | // act 70 | cors({ optionsSuccessStatus: 200 })(req, res, function (err) { 71 | cb(err || new Error('should not be called')) 72 | }) 73 | }); 74 | 75 | it('doesn\'t shortcircuit preflight requests with preflightContinue option', function (done) { 76 | var cb = after(1, done) 77 | var req = new FakeRequest('OPTIONS') 78 | var res = new FakeResponse() 79 | 80 | res.on('finish', function () { 81 | cb(new Error('should not be called')) 82 | }) 83 | 84 | cors({ preflightContinue: true })(req, res, function (err) { 85 | if (err) return cb(err) 86 | setTimeout(cb, 10) 87 | }) 88 | }); 89 | 90 | it('normalizes method names', function (done) { 91 | var cb = after(1, done) 92 | var req = new FakeRequest('options') 93 | var res = new FakeResponse() 94 | 95 | res.on('finish', function () { 96 | assert.equal(res.statusCode, 204) 97 | cb() 98 | }) 99 | 100 | cors()(req, res, function (err) { 101 | cb(err || new Error('should not be called')) 102 | }) 103 | }); 104 | 105 | it('includes Content-Length response header', function (done) { 106 | var cb = after(1, done) 107 | var req = new FakeRequest('OPTIONS') 108 | var res = new FakeResponse() 109 | 110 | res.on('finish', function () { 111 | assert.equal(res.getHeader('Content-Length'), '0') 112 | cb() 113 | }) 114 | 115 | cors()(req, res, function (err) { 116 | cb(err || new Error('should not be called')) 117 | }) 118 | }); 119 | 120 | it('no options enables default CORS to all origins', function (done) { 121 | // arrange 122 | var req, res, next; 123 | req = fakeRequest('GET'); 124 | res = fakeResponse(); 125 | next = function () { 126 | // assert 127 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), '*') 128 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), undefined) 129 | done(); 130 | }; 131 | 132 | // act 133 | cors()(req, res, next); 134 | }); 135 | 136 | it('OPTION call with no options enables default CORS to all origins and methods', function (done) { 137 | var cb = after(1, done) 138 | var req = new FakeRequest('OPTIONS') 139 | var res = new FakeResponse() 140 | 141 | res.on('finish', function () { 142 | assert.equal(res.statusCode, 204) 143 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), '*') 144 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), 'GET,HEAD,PUT,PATCH,POST,DELETE') 145 | cb() 146 | }) 147 | 148 | cors()(req, res, function (err) { 149 | cb(err || new Error('should not be called')) 150 | }) 151 | }); 152 | 153 | describe('passing static options', function () { 154 | it('overrides defaults', function (done) { 155 | var cb = after(1, done) 156 | var req = new FakeRequest('OPTIONS') 157 | var res = new FakeResponse() 158 | var options = { 159 | origin: 'http://example.com', 160 | methods: ['FOO', 'bar'], 161 | headers: ['FIZZ', 'buzz'], 162 | credentials: true, 163 | maxAge: 123 164 | }; 165 | 166 | res.on('finish', function () { 167 | assert.equal(res.statusCode, 204) 168 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'http://example.com') 169 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), 'FOO,bar') 170 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), 'FIZZ,buzz') 171 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), 'true') 172 | assert.equal(res.getHeader('Access-Control-Max-Age'), '123') 173 | cb() 174 | }) 175 | 176 | cors(options)(req, res, function (err) { 177 | cb(err || new Error('should not be called')) 178 | }) 179 | }); 180 | 181 | it('matches request origin against regexp', function(done) { 182 | var req = fakeRequest('GET'); 183 | var res = fakeResponse(); 184 | var options = { origin: /:\/\/(.+\.)?example.com$/ } 185 | cors(options)(req, res, function(err) { 186 | assert.ifError(err) 187 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), req.headers.origin) 188 | assert.equal(res.getHeader('Vary'), 'Origin') 189 | return done(); 190 | }); 191 | }); 192 | 193 | it('matches request origin against array of origin checks', function(done) { 194 | var req = fakeRequest('GET'); 195 | var res = fakeResponse(); 196 | var options = { origin: [ /foo\.com$/, 'http://example.com' ] } 197 | cors(options)(req, res, function(err) { 198 | assert.ifError(err) 199 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), req.headers.origin) 200 | assert.equal(res.getHeader('Vary'), 'Origin') 201 | return done(); 202 | }); 203 | }); 204 | 205 | it('doesn\'t match request origin against array of invalid origin checks', function(done) { 206 | var req = fakeRequest('GET'); 207 | var res = fakeResponse(); 208 | var options = { origin: [ /foo\.com$/, 'bar.com' ] }; 209 | cors(options)(req, res, function(err) { 210 | assert.ifError(err) 211 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), undefined) 212 | assert.equal(res.getHeader('Vary'), 'Origin') 213 | return done(); 214 | }); 215 | }); 216 | 217 | it('origin of false disables cors', function (done) { 218 | // arrange 219 | var req, res, next, options; 220 | options = { 221 | origin: false, 222 | methods: ['FOO', 'bar'], 223 | headers: ['FIZZ', 'buzz'], 224 | credentials: true, 225 | maxAge: 123 226 | }; 227 | req = fakeRequest('GET'); 228 | res = fakeResponse(); 229 | next = function () { 230 | // assert 231 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), undefined) 232 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), undefined) 233 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), undefined) 234 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), undefined) 235 | assert.equal(res.getHeader('Access-Control-Max-Age'), undefined) 236 | done(); 237 | }; 238 | 239 | // act 240 | cors(options)(req, res, next); 241 | }); 242 | 243 | it('can override origin', function (done) { 244 | // arrange 245 | var req, res, next, options; 246 | options = { 247 | origin: 'http://example.com' 248 | }; 249 | req = fakeRequest('GET'); 250 | res = fakeResponse(); 251 | next = function () { 252 | // assert 253 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'http://example.com') 254 | done(); 255 | }; 256 | 257 | // act 258 | cors(options)(req, res, next); 259 | }); 260 | 261 | it('includes Vary header for specific origins', function (done) { 262 | // arrange 263 | var req, res, next, options; 264 | options = { 265 | origin: 'http://example.com' 266 | }; 267 | req = fakeRequest('GET'); 268 | res = fakeResponse(); 269 | next = function () { 270 | // assert 271 | assert.equal(res.getHeader('Vary'), 'Origin') 272 | done(); 273 | }; 274 | 275 | // act 276 | cors(options)(req, res, next); 277 | }); 278 | 279 | it('appends to an existing Vary header', function (done) { 280 | // arrange 281 | var req, res, next, options; 282 | options = { 283 | origin: 'http://example.com' 284 | }; 285 | req = fakeRequest('GET'); 286 | res = fakeResponse(); 287 | res.setHeader('Vary', 'Foo'); 288 | next = function () { 289 | // assert 290 | assert.equal(res.getHeader('Vary'), 'Foo, Origin') 291 | done(); 292 | }; 293 | 294 | // act 295 | cors(options)(req, res, next); 296 | }); 297 | 298 | it('origin defaults to *', function (done) { 299 | // arrange 300 | var req, res, next; 301 | req = fakeRequest('GET'); 302 | res = fakeResponse(); 303 | next = function () { 304 | // assert 305 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), '*') 306 | done(); 307 | }; 308 | 309 | // act 310 | cors()(req, res, next); 311 | }); 312 | 313 | it('specifying true for origin reflects requesting origin', function (done) { 314 | // arrange 315 | var req, res, next, options; 316 | options = { 317 | origin: true 318 | }; 319 | req = fakeRequest('GET'); 320 | res = fakeResponse(); 321 | next = function () { 322 | // assert 323 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'http://example.com') 324 | done(); 325 | }; 326 | 327 | // act 328 | cors(options)(req, res, next); 329 | }); 330 | 331 | it('should allow origin when callback returns true', function (done) { 332 | var req, res, next, options; 333 | options = { 334 | origin: function (sentOrigin, cb) { 335 | cb(null, true); 336 | } 337 | }; 338 | req = fakeRequest('GET'); 339 | res = fakeResponse(); 340 | next = function () { 341 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'http://example.com') 342 | done(); 343 | }; 344 | 345 | cors(options)(req, res, next); 346 | }); 347 | 348 | it('should not allow origin when callback returns false', function (done) { 349 | var req, res, next, options; 350 | options = { 351 | origin: function (sentOrigin, cb) { 352 | cb(null, false); 353 | } 354 | }; 355 | req = fakeRequest('GET'); 356 | res = fakeResponse(); 357 | next = function () { 358 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), undefined) 359 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), undefined) 360 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), undefined) 361 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), undefined) 362 | assert.equal(res.getHeader('Access-Control-Max-Age'), undefined) 363 | done(); 364 | }; 365 | 366 | cors(options)(req, res, next); 367 | }); 368 | 369 | it('should not override options.origin callback', function (done) { 370 | var req, res, next, options; 371 | options = { 372 | origin: function (sentOrigin, cb) { 373 | cb(null, sentOrigin === 'http://example.com') 374 | } 375 | }; 376 | 377 | req = fakeRequest('GET'); 378 | res = fakeResponse(); 379 | next = function () { 380 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'http://example.com') 381 | }; 382 | 383 | cors(options)(req, res, next); 384 | 385 | req = fakeRequest('GET', { 386 | 'origin': 'http://localhost' 387 | }); 388 | res = fakeResponse(); 389 | 390 | next = function () { 391 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), undefined) 392 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), undefined) 393 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), undefined) 394 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), undefined) 395 | assert.equal(res.getHeader('Access-Control-Max-Age'), undefined) 396 | done(); 397 | }; 398 | 399 | cors(options)(req, res, next); 400 | }); 401 | 402 | 403 | it('can override methods', function (done) { 404 | var cb = after(1, done) 405 | var req = new FakeRequest('OPTIONS') 406 | var res = new FakeResponse() 407 | var options = { 408 | methods: ['method1', 'method2'] 409 | }; 410 | 411 | res.on('finish', function () { 412 | assert.equal(res.statusCode, 204) 413 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), 'method1,method2') 414 | cb() 415 | }) 416 | 417 | cors(options)(req, res, function (err) { 418 | cb(err || new Error('should not be called')) 419 | }) 420 | }); 421 | 422 | it('methods defaults to GET, HEAD, PUT, PATCH, POST, DELETE', function (done) { 423 | var cb = after(1, done) 424 | var req = new FakeRequest('OPTIONS') 425 | var res = new FakeResponse() 426 | 427 | res.on('finish', function () { 428 | assert.equal(res.statusCode, 204) 429 | assert.equal(res.getHeader('Access-Control-Allow-Methods'), 'GET,HEAD,PUT,PATCH,POST,DELETE') 430 | cb() 431 | }) 432 | 433 | cors()(req, res, function (err) { 434 | cb(err || new Error('should not be called')) 435 | }) 436 | }); 437 | 438 | it('can specify allowed headers as array', function (done) { 439 | var cb = after(1, done) 440 | var req = new FakeRequest('OPTIONS') 441 | var res = new FakeResponse() 442 | 443 | res.on('finish', function () { 444 | assert.strictEqual(res.getHeader('Access-Control-Allow-Headers'), 'header1,header2') 445 | assert.equal(res.getHeader('Vary'), undefined) 446 | cb() 447 | }) 448 | 449 | cors({ allowedHeaders: ['header1', 'header2'] })(req, res, function (err) { 450 | cb(err || new Error('should not be called')) 451 | }) 452 | }); 453 | 454 | it('can specify allowed headers as string', function (done) { 455 | var cb = after(1, done) 456 | var req = new FakeRequest('OPTIONS') 457 | var res = new FakeResponse() 458 | 459 | res.on('finish', function () { 460 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), 'header1,header2') 461 | assert.equal(res.getHeader('Vary'), undefined) 462 | cb() 463 | }) 464 | 465 | cors({ allowedHeaders: 'header1,header2' })(req, res, function (err) { 466 | cb(err || new Error('should not be called')) 467 | }) 468 | }); 469 | 470 | it('specifying an empty list or string of allowed headers will result in no response header for allowed headers', function (done) { 471 | // arrange 472 | var req, res, next, options; 473 | options = { 474 | allowedHeaders: [] 475 | }; 476 | req = fakeRequest('GET'); 477 | res = fakeResponse(); 478 | next = function () { 479 | // assert 480 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), undefined) 481 | assert.equal(res.getHeader('Vary'), undefined) 482 | done(); 483 | }; 484 | 485 | // act 486 | cors(options)(req, res, next); 487 | }); 488 | 489 | it('if no allowed headers are specified, defaults to requested allowed headers', function (done) { 490 | var cb = after(1, done) 491 | var req = new FakeRequest('OPTIONS') 492 | var res = new FakeResponse() 493 | 494 | res.on('finish', function () { 495 | assert.equal(res.getHeader('Access-Control-Allow-Headers'), 'x-header-1, x-header-2') 496 | assert.equal(res.getHeader('Vary'), 'Access-Control-Request-Headers') 497 | cb() 498 | }) 499 | 500 | cors()(req, res, function (err) { 501 | cb(err || new Error('should not be called')) 502 | }) 503 | }); 504 | 505 | it('can specify exposed headers as array', function (done) { 506 | // arrange 507 | var req, res, options, next; 508 | options = { 509 | exposedHeaders: ['custom-header1', 'custom-header2'] 510 | }; 511 | req = fakeRequest('GET'); 512 | res = fakeResponse(); 513 | next = function () { 514 | // assert 515 | assert.equal(res.getHeader('Access-Control-Expose-Headers'), 'custom-header1,custom-header2') 516 | done(); 517 | }; 518 | 519 | // act 520 | cors(options)(req, res, next); 521 | }); 522 | 523 | it('can specify exposed headers as string', function (done) { 524 | // arrange 525 | var req, res, options, next; 526 | options = { 527 | exposedHeaders: 'custom-header1,custom-header2' 528 | }; 529 | req = fakeRequest('GET'); 530 | res = fakeResponse(); 531 | next = function () { 532 | // assert 533 | assert.equal(res.getHeader('Access-Control-Expose-Headers'), 'custom-header1,custom-header2') 534 | done(); 535 | }; 536 | 537 | // act 538 | cors(options)(req, res, next); 539 | }); 540 | 541 | it('specifying an empty list or string of exposed headers will result in no response header for exposed headers', function (done) { 542 | // arrange 543 | var req, res, next, options; 544 | options = { 545 | exposedHeaders: [] 546 | }; 547 | req = fakeRequest('GET'); 548 | res = fakeResponse(); 549 | next = function () { 550 | // assert 551 | assert.equal(res.getHeader('Access-Control-Expose-Headers'), undefined) 552 | done(); 553 | }; 554 | 555 | // act 556 | cors(options)(req, res, next); 557 | }); 558 | 559 | it('includes credentials if explicitly enabled', function (done) { 560 | var cb = after(1, done) 561 | var req = new FakeRequest('OPTIONS') 562 | var res = new FakeResponse() 563 | 564 | res.on('finish', function () { 565 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), 'true') 566 | cb() 567 | }) 568 | 569 | cors({ credentials: true })(req, res, function (err) { 570 | cb(err || new Error('should not be called')) 571 | }) 572 | }); 573 | 574 | it('does not includes credentials unless explicitly enabled', function (done) { 575 | // arrange 576 | var req, res, next; 577 | req = fakeRequest('GET'); 578 | res = fakeResponse(); 579 | next = function () { 580 | // assert 581 | assert.equal(res.getHeader('Access-Control-Allow-Credentials'), undefined) 582 | done(); 583 | }; 584 | 585 | // act 586 | cors()(req, res, next); 587 | }); 588 | 589 | it('includes maxAge when specified', function (done) { 590 | var cb = after(1, done) 591 | var req = new FakeRequest('OPTIONS') 592 | var res = new FakeResponse() 593 | 594 | res.on('finish', function () { 595 | assert.equal(res.getHeader('Access-Control-Max-Age'), '456') 596 | cb() 597 | }) 598 | 599 | cors({ maxAge: 456 })(req, res, function (err) { 600 | cb(err || new Error('should not be called')) 601 | }) 602 | }); 603 | 604 | it('includes maxAge when specified and equals to zero', function (done) { 605 | var cb = after(1, done) 606 | var req = new FakeRequest('OPTIONS') 607 | var res = new FakeResponse() 608 | 609 | res.on('finish', function () { 610 | assert.equal(res.getHeader('Access-Control-Max-Age'), '0') 611 | cb() 612 | }) 613 | 614 | cors({ maxAge: 0 })(req, res, function (err) { 615 | cb(err || new Error('should not be called')) 616 | }) 617 | }); 618 | 619 | it('does not includes maxAge unless specified', function (done) { 620 | // arrange 621 | var req, res, next; 622 | req = fakeRequest('GET'); 623 | res = fakeResponse(); 624 | next = function () { 625 | // assert 626 | assert.equal(res.getHeader('Access-Control-Max-Age'), undefined) 627 | done(); 628 | }; 629 | 630 | // act 631 | cors()(req, res, next); 632 | }); 633 | }); 634 | 635 | describe('passing a function to build options', function () { 636 | it('handles options specified via callback', function (done) { 637 | // arrange 638 | var req, res, next, delegate; 639 | delegate = function (req2, cb) { 640 | cb(null, { 641 | origin: 'delegate.com' 642 | }); 643 | }; 644 | req = fakeRequest('GET'); 645 | res = fakeResponse(); 646 | next = function () { 647 | // assert 648 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'delegate.com') 649 | done(); 650 | }; 651 | 652 | // act 653 | cors(delegate)(req, res, next); 654 | }); 655 | 656 | it('handles options specified via callback for preflight', function (done) { 657 | var cb = after(1, done) 658 | var req = new FakeRequest('OPTIONS') 659 | var res = new FakeResponse() 660 | var delegate = function (req2, cb) { 661 | cb(null, { 662 | origin: 'delegate.com', 663 | maxAge: 1000 664 | }); 665 | }; 666 | 667 | res.on('finish', function () { 668 | assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'delegate.com') 669 | assert.equal(res.getHeader('Access-Control-Max-Age'), '1000') 670 | cb() 671 | }) 672 | 673 | cors(delegate)(req, res, function (err) { 674 | cb(err || new Error('should not be called')) 675 | }) 676 | }); 677 | 678 | it('handles error specified via callback', function (done) { 679 | // arrange 680 | var req, res, next, delegate; 681 | delegate = function (req2, cb) { 682 | cb('some error'); 683 | }; 684 | req = fakeRequest('GET'); 685 | res = fakeResponse(); 686 | next = function (err) { 687 | // assert 688 | assert.equal(err, 'some error') 689 | done(); 690 | }; 691 | 692 | // act 693 | cors(delegate)(req, res, next); 694 | }); 695 | }); 696 | }); 697 | 698 | }()); 699 | 700 | function FakeRequest (method, headers) { 701 | this.headers = headers || { 702 | 'origin': 'http://example.com', 703 | 'access-control-request-headers': 'x-header-1, x-header-2' 704 | } 705 | this.method = method || 'GET' 706 | } 707 | 708 | function FakeResponse () { 709 | this._headers = {} 710 | this.statusCode = 200 711 | } 712 | 713 | util.inherits(FakeResponse, EventEmitter) 714 | 715 | FakeResponse.prototype.end = function end () { 716 | var response = this 717 | 718 | process.nextTick(function () { 719 | response.emit('finish') 720 | }) 721 | } 722 | 723 | FakeResponse.prototype.getHeader = function getHeader (name) { 724 | var key = name.toLowerCase() 725 | return this._headers[key] 726 | } 727 | 728 | FakeResponse.prototype.setHeader = function setHeader (name, value) { 729 | var key = name.toLowerCase() 730 | this._headers[key] = value 731 | } 732 | --------------------------------------------------------------------------------