├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── cookie.js ├── express.js ├── fixtures ├── server.crt └── server.key ├── restify.js ├── support └── env.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.js,*.json,*.yml}] 10 | indent_size = 2 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | rules: 3 | eol-last: error 4 | indent: ["error", 2, { "SwitchCase": 1 }] 5 | no-param-reassign: error 6 | no-trailing-spaces: error 7 | no-unused-vars: ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }] 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.8 14 | - Node.js 0.10 15 | - Node.js 0.12 16 | - io.js 1.x 17 | - io.js 2.x 18 | - io.js 3.x 19 | - Node.js 4.x 20 | - Node.js 5.x 21 | - Node.js 6.x 22 | - Node.js 7.x 23 | - Node.js 8.x 24 | - Node.js 9.x 25 | - Node.js 10.x 26 | - Node.js 11.x 27 | - Node.js 12.x 28 | - Node.js 13.x 29 | - Node.js 14.x 30 | - Node.js 15.x 31 | - Node.js 16.x 32 | - Node.js 17.x 33 | - Node.js 18.x 34 | - Node.js 19.x 35 | - Node.js 20.x 36 | - Node.js 21.x 37 | 38 | include: 39 | - name: Node.js 0.8 40 | node-version: "0.8" 41 | npm-i: mocha@2.5.3 supertest@1.1.0 42 | npm-rm: express nyc restify 43 | 44 | - name: Node.js 0.10 45 | node-version: "0.10" 46 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 47 | npm-rm: restify 48 | 49 | - name: Node.js 0.12 50 | node-version: "0.12" 51 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 52 | npm-rm: restify 53 | 54 | - name: io.js 1.x 55 | node-version: "1.8" 56 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 57 | npm-rm: restify 58 | 59 | - name: io.js 2.x 60 | node-version: "2.5" 61 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 62 | npm-rm: restify 63 | 64 | - name: io.js 3.x 65 | node-version: "3.3" 66 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 67 | npm-rm: restify 68 | 69 | - name: Node.js 4.x 70 | node-version: "4.9" 71 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 72 | npm-rm: restify 73 | 74 | - name: Node.js 5.x 75 | node-version: "5.12" 76 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 77 | npm-rm: restify 78 | 79 | - name: Node.js 6.x 80 | node-version: "6.17" 81 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2 82 | npm-rm: restify 83 | 84 | - name: Node.js 7.x 85 | node-version: "7.10" 86 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 87 | npm-rm: restify 88 | 89 | - name: Node.js 8.x 90 | node-version: "8.17" 91 | npm-i: mocha@7.2.0 nyc@14.1.1 92 | 93 | - name: Node.js 9.x 94 | node-version: "9.11" 95 | npm-i: mocha@7.2.0 nyc@14.1.1 96 | 97 | - name: Node.js 10.x 98 | node-version: "10.24" 99 | npm-i: mocha@8.4.0 100 | 101 | - name: Node.js 11.x 102 | node-version: "11.15" 103 | npm-i: mocha@8.4.0 104 | 105 | - name: Node.js 12.x 106 | node-version: "12.22" 107 | npm-i: mocha@9.2.2 108 | 109 | - name: Node.js 13.x 110 | node-version: "13.14" 111 | npm-i: mocha@9.2.2 112 | 113 | - name: Node.js 14.x 114 | node-version: "14.21" 115 | 116 | - name: Node.js 15.x 117 | node-version: "15.14" 118 | 119 | - name: Node.js 16.x 120 | node-version: "16.20" 121 | 122 | - name: Node.js 17.x 123 | node-version: "17.9" 124 | 125 | - name: Node.js 18.x 126 | node-version: "18.19" 127 | 128 | - name: Node.js 19.x 129 | node-version: "19.9" 130 | 131 | - name: Node.js 20.x 132 | node-version: "20.10" 133 | 134 | - name: Node.js 21.x 135 | node-version: "21.4" 136 | 137 | steps: 138 | - uses: actions/checkout@v3 139 | 140 | - name: Install Node.js ${{ matrix.node-version }} 141 | shell: bash -eo pipefail -l {0} 142 | run: | 143 | nvm install --default ${{ matrix.node-version }} 144 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 145 | nvm install --alias=npm 0.10 146 | nvm use ${{ matrix.node-version }} 147 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 148 | npm config set strict-ssl false 149 | fi 150 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 151 | 152 | - name: Configure npm 153 | run: | 154 | if [[ "$(npm config get package-lock)" == "true" ]]; then 155 | npm config set package-lock false 156 | else 157 | npm config set shrinkwrap false 158 | fi 159 | 160 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 161 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 162 | if: matrix.npm-rm != '' 163 | 164 | - name: Install npm module(s) ${{ matrix.npm-i }} 165 | run: npm install --save-dev ${{ matrix.npm-i }} 166 | if: matrix.npm-i != '' 167 | 168 | - name: Setup Node.js version-specific dependencies 169 | shell: bash 170 | run: | 171 | # eslint for linting 172 | # - remove on Node.js < 12 173 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 174 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 175 | grep -E '^eslint(-|$)' | \ 176 | sort -r | \ 177 | xargs -n1 npm rm --silent --save-dev 178 | fi 179 | 180 | - name: Install Node.js dependencies 181 | run: npm install 182 | 183 | - name: List environment 184 | id: list_env 185 | shell: bash 186 | run: | 187 | echo "node@$(node -v)" 188 | echo "npm@$(npm -v)" 189 | npm -s ls ||: 190 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 191 | 192 | - name: Run tests 193 | shell: bash 194 | run: | 195 | if npm -ps ls nyc | grep -q nyc; then 196 | npm run test-ci 197 | cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" 198 | else 199 | npm test 200 | fi 201 | 202 | - name: Lint code 203 | if: steps.list_env.outputs.eslint != '' 204 | run: npm run lint 205 | 206 | - name: Collect code coverage 207 | if: steps.list_env.outputs.nyc != '' 208 | run: | 209 | if [[ -d ./coverage ]]; then 210 | mv ./coverage "./${{ matrix.name }}" 211 | mkdir ./coverage 212 | mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}" 213 | fi 214 | 215 | - name: Upload code coverage 216 | uses: actions/upload-artifact@v3 217 | if: steps.list_env.outputs.nyc != '' 218 | with: 219 | name: coverage 220 | path: ./coverage 221 | retention-days: 1 222 | 223 | coverage: 224 | needs: test 225 | runs-on: ubuntu-latest 226 | steps: 227 | - uses: actions/checkout@v3 228 | 229 | - name: Install lcov 230 | shell: bash 231 | run: sudo apt-get -y install lcov 232 | 233 | - name: Collect coverage reports 234 | uses: actions/download-artifact@v3 235 | with: 236 | name: coverage 237 | path: ./coverage 238 | 239 | - name: Merge coverage reports 240 | shell: bash 241 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info 242 | 243 | - name: Upload coverage report 244 | uses: coverallsapp/github-action@master 245 | with: 246 | github-token: ${{ secrets.GITHUB_TOKEN }} 247 | -------------------------------------------------------------------------------- /.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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 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@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 0.9.1 / 2024-01-01 2 | ================== 3 | 4 | * Fix incorrectly disallowing equals sign in cookie value 5 | 6 | 0.9.0 / 2023-12-28 7 | ================== 8 | 9 | * Add `partitioned` option for CHIPS support 10 | * Add `priority` option for Priority cookie support 11 | * Fix accidental cookie name/value truncation when given invalid chars 12 | * Fix `maxAge` option to reject invalid values 13 | * Remove quotes from returned quoted cookie value 14 | * Use `req.socket` over deprecated `req.connection` 15 | * pref: small lookup regexp optimization 16 | 17 | 0.8.0 / 2019-10-11 18 | ================== 19 | 20 | * Fix check for default `secure` option behavior 21 | * Fix `maxAge` option preventing cookie deletion 22 | * Support `"none"` in `sameSite` option 23 | * deps: depd@~2.0.0 24 | - Replace internal `eval` usage with `Function` constructor 25 | - Use instance methods on `process` to check for listeners 26 | * deps: keygrip@~1.1.0 27 | - Use `tsscmp` module for timing-safe signature verification 28 | 29 | 0.7.3 / 2018-11-04 30 | ================== 31 | 32 | * deps: keygrip@~1.0.3 33 | - perf: enable strict mode 34 | 35 | 0.7.2 / 2018-09-09 36 | ================== 37 | 38 | * deps: depd@~1.1.2 39 | * perf: remove argument reassignment 40 | 41 | 0.7.1 / 2017-08-26 42 | ================== 43 | 44 | * deps: depd@~1.1.1 45 | - Remove unnecessary `Buffer` loading 46 | * deps: keygrip@~1.0.2 47 | - perf: improve comparison speed 48 | 49 | 0.7.0 / 2017-02-19 50 | ================== 51 | 52 | * Add `sameSite` option for SameSite cookie support 53 | * pref: enable strict mode 54 | 55 | 0.6.2 / 2016-11-12 56 | ================== 57 | 58 | * Fix `keys` deprecation message 59 | * deps: keygrip@~1.0.1 60 | 61 | 0.6.1 / 2016-02-29 62 | ================== 63 | 64 | * Fix regression in 0.6.0 for array of strings in `keys` option 65 | 66 | 0.6.0 / 2016-02-29 67 | ================== 68 | 69 | * Add `secure` constructor option for secure connection checking 70 | * Change constructor to signature `new Cookies(req, res, [options])` 71 | - Replace `new Cookies(req, res, key)` with `new Cookies(req, res, {'keys': keys})` 72 | * Change prototype construction for proper "constructor" property 73 | * Deprecate `secureProxy` option in `.set`; use `secure` option instead 74 | - If `secure: true` throws even over SSL, use the `secure` constructor option 75 | 76 | 0.5.1 / 2014-07-27 77 | ================== 78 | 79 | * Throw on invalid values provided to `Cookie` constructor 80 | - This is not strict validation, but basic RFC 7230 validation 81 | 82 | 0.5.0 / 2014-07-27 83 | ================== 84 | 85 | * Integrate with `req.protocol` for secure cookies 86 | * Support `maxAge` as well as `maxage` 87 | 88 | 0.4.1 / 2014-05-07 89 | ================== 90 | 91 | * Update package for repo move 92 | 93 | 0.4.0 / 2014-01-31 94 | ================== 95 | 96 | * Allow passing an array of strings as keys 97 | 98 | 0.3.8-0.2.0 99 | =========== 100 | 101 | * TODO: write down history for these releases 102 | 103 | 0.1.6 / 2011-03-01 104 | ================== 105 | 106 | * SSL cookies secure by default 107 | * Use httpOnly by default unless explicitly false 108 | 109 | 0.1.5 / 2011-02-26 110 | ================== 111 | 112 | * Delete sig cookie if signed cookie is deleted 113 | 114 | 0.1.4 / 2011-02-26 115 | ================== 116 | 117 | * Always set path 118 | 119 | 0.1.3 / 2011-02-26 120 | ================== 121 | 122 | * Add sensible defaults for path 123 | 124 | 0.1.2 / 2011-02-26 125 | ================== 126 | 127 | * Inherit cookie properties to signature cookie 128 | 129 | 0.1.1 / 2011-02-25 130 | ================== 131 | 132 | * Readme updates 133 | 134 | 0.1.0 / 2011-02-25 135 | ================== 136 | 137 | * Initial release 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jed Schmidt, http://jed.is/ 4 | Copyright (c) 2015-2016 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cookies 2 | ======= 3 | 4 | [![NPM Version][npm-image]][npm-url] 5 | [![NPM Downloads][downloads-image]][downloads-url] 6 | [![Node.js Version][node-version-image]][node-version-url] 7 | [![Build Status][ci-image]][ci-url] 8 | [![Test Coverage][coveralls-image]][coveralls-url] 9 | 10 | Cookies is a [node.js](http://nodejs.org/) module for getting and setting HTTP(S) cookies. Cookies can be signed to prevent tampering, using [Keygrip](https://www.npmjs.com/package/keygrip). It can be used with the built-in node.js HTTP library, or as Connect/Express middleware. 11 | 12 | ## Install 13 | 14 | This is a [Node.js](https://nodejs.org/en/) module available through the 15 | [npm registry](https://www.npmjs.com/). Installation is done using the 16 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 17 | 18 | ``` 19 | $ npm install cookies 20 | ``` 21 | 22 | ## Features 23 | 24 | * **Lazy**: Since cookie verification against multiple keys could be expensive, cookies are only verified lazily when accessed, not eagerly on each request. 25 | 26 | * **Secure**: All cookies are `httponly` by default, and cookies sent over SSL are `secure` by default. An error will be thrown if you try to send secure cookies over an insecure socket. 27 | 28 | * **Unobtrusive**: Signed cookies are stored the same way as unsigned cookies, instead of in an obfuscated signing format. An additional signature cookie is stored for each signed cookie, using a standard naming convention (_cookie-name_`.sig`). This allows other libraries to access the original cookies without having to know the signing mechanism. 29 | 30 | * **Agnostic**: This library is optimized for use with [Keygrip](https://www.npmjs.com/package/keygrip), but does not require it; you can implement your own signing scheme instead if you like and use this library only to read/write cookies. Factoring the signing into a separate library encourages code reuse and allows you to use the same signing library for other areas where signing is needed, such as in URLs. 31 | 32 | ## API 33 | 34 | ### new Cookies(request, response [, options]) 35 | 36 | Create a new cookie jar for a given `request` and `response` pair. The `request` argument is a [Node.js HTTP incoming request object](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#class-httpincomingmessage) and the `response` argument is a [Node.js HTTP server response object](https://nodejs.org/dist/latest-v16.x/docs/api/http.html#class-httpserverresponse). 37 | 38 | A [Keygrip](https://www.npmjs.com/package/keygrip) object or an array of keys can optionally be passed as `options.keys` to enable cryptographic signing based on SHA1 HMAC, using rotated credentials. 39 | 40 | A Boolean can optionally be passed as `options.secure` to explicitally specify if the connection is secure, rather than this module examining `request`. 41 | 42 | Note that since this only saves parameters without any other processing, it is very lightweight. Cookies are only parsed on demand when they are accessed. 43 | 44 | ### Cookies.express(keys) 45 | 46 | This adds cookie support as a Connect middleware layer for use in Express apps, allowing inbound cookies to be read using `req.cookies.get` and outbound cookies to be set using `res.cookies.set`. 47 | 48 | ### cookies.get(name [, options]) 49 | 50 | This extracts the cookie with the given name from the `Cookie` header in the request. If such a cookie exists, its value is returned. Otherwise, nothing is returned. 51 | 52 | `{ signed: true }` can optionally be passed as the second parameter _options_. In this case, a signature cookie (a cookie of same name ending with the `.sig` suffix appended) is fetched. If no such cookie exists, nothing is returned. 53 | 54 | If the signature cookie _does_ exist, the provided [Keygrip](https://www.npmjs.com/package/keygrip) object is used to check whether the hash of _cookie-name_=_cookie-value_ matches that of any registered key: 55 | 56 | * If the signature cookie hash matches the first key, the original cookie value is returned. 57 | * If the signature cookie hash matches any other key, the original cookie value is returned AND an outbound header is set to update the signature cookie's value to the hash of the first key. This enables automatic freshening of signature cookies that have become stale due to key rotation. 58 | * If the signature cookie hash does not match any key, nothing is returned, and an outbound header with an expired date is used to delete the cookie. 59 | 60 | ### cookies.set(name [, values [, options]]) 61 | 62 | This sets the given cookie in the response and returns the current context to allow chaining. 63 | 64 | If the _value_ is omitted, an outbound header with an expired date is used to delete the cookie. 65 | 66 | If the _options_ object is provided, it will be used to generate the outbound cookie header as follows: 67 | 68 | * `maxAge`: a number representing the milliseconds from `Date.now()` for expiry 69 | * `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default). 70 | * `path`: a string indicating the path of the cookie (`/` by default). 71 | * `domain`: a string indicating the domain of the cookie (no default). 72 | * `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). [Read more about this option below](#secure-cookies). 73 | * `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default). 74 | * `partitioned`: a boolean indicating whether to partition the cookie in Chrome for the [CHIPS Update](https://developers.google.com/privacy-sandbox/3pcd/chips) (`false` by default). If this is true, Cookies from embedded sites will be partitioned and only readable from the same top level site from which it was created. 75 | * `priority`: a string indicating the cookie priority. This can be set to `'low'`, `'medium'`, or `'high'`. 76 | * `sameSite`: a boolean or string indicating whether the cookie is a "same site" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`). 77 | * `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received. 78 | * `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie. 79 | 80 | ### Secure cookies 81 | 82 | To send a secure cookie, you set a cookie with the `secure: true` option. 83 | 84 | HTTPS is necessary for secure cookies. When `cookies.set` is called with `secure: true` and a secure connection is not detected, the cookie will not be set and an error will be thrown. 85 | 86 | This module will test each request to see if it's secure by checking: 87 | 88 | * if the `protocol` property of the request is set to `https`, or 89 | * if the `connection.encrypted` property of the request is set to `true`. 90 | 91 | If your server is running behind a proxy and you are using `secure: true`, you need to configure your server to read the request headers added by your proxy to determine whether the request is using a secure connection. 92 | 93 | For more information about working behind proxies, consult the framework you are using: 94 | 95 | * For Koa - [`app.proxy = true`](http://koajs.com/#settings) 96 | * For Express - [trust proxy setting](http://expressjs.com/en/4x/api.html#trust.proxy.options.table) 97 | 98 | If your Koa or Express server is properly configured, the `protocol` property of the request will be set to match the protocol reported by the proxy in the `X-Forwarded-Proto` header. 99 | 100 | ## Example 101 | 102 | ```js 103 | var http = require('http') 104 | var Cookies = require('cookies') 105 | 106 | // Optionally define keys to sign cookie values 107 | // to prevent client tampering 108 | var keys = ['keyboard cat'] 109 | 110 | var server = http.createServer(function (req, res) { 111 | // Create a cookies object 112 | var cookies = new Cookies(req, res, { keys: keys }) 113 | 114 | // Get a cookie 115 | var lastVisit = cookies.get('LastVisit', { signed: true }) 116 | 117 | // Set the cookie to a value 118 | cookies.set('LastVisit', new Date().toISOString(), { signed: true }) 119 | 120 | if (!lastVisit) { 121 | res.setHeader('Content-Type', 'text/plain') 122 | res.end('Welcome, first time visitor!') 123 | } else { 124 | res.setHeader('Content-Type', 'text/plain') 125 | res.end('Welcome back! Nothing much changed since your last visit at ' + lastVisit + '.') 126 | } 127 | }) 128 | 129 | server.listen(3000, function () { 130 | console.log('Visit us at http://127.0.0.1:3000/ !') 131 | }) 132 | ``` 133 | 134 | ## License 135 | 136 | [MIT](LICENSE) 137 | 138 | [ci-image]: https://badgen.net/github/checks/pillarjs/cookies/master?label=ci 139 | [ci-url]: https://github.com/pillarjs/cookies/actions/workflows/ci.yml 140 | [npm-image]: https://img.shields.io/npm/v/cookies.svg 141 | [npm-url]: https://npmjs.org/package/cookies 142 | [node-version-image]: https://img.shields.io/node/v/cookies.svg 143 | [node-version-url]: http://nodejs.org/download/ 144 | [coveralls-image]: https://img.shields.io/coveralls/pillarjs/cookies/master.svg 145 | [coveralls-url]: https://coveralls.io/r/pillarjs/cookies?branch=master 146 | [downloads-image]: https://img.shields.io/npm/dm/cookies.svg 147 | [downloads-url]: https://npmjs.org/package/cookies 148 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * cookies 3 | * Copyright(c) 2014 Jed Schmidt, http://jed.is/ 4 | * Copyright(c) 2015-2016 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | var deprecate = require('depd')('cookies') 11 | var Keygrip = require('keygrip') 12 | var http = require('http') 13 | 14 | /** 15 | * RegExp to match field-content in RFC 7230 sec 3.2 16 | * 17 | * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 18 | * field-vchar = VCHAR / obs-text 19 | * obs-text = %x80-FF 20 | */ 21 | 22 | var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; 23 | 24 | /** 25 | * RegExp to match Priority cookie attribute value. 26 | */ 27 | 28 | var PRIORITY_REGEXP = /^(?:low|medium|high)$/i 29 | 30 | /** 31 | * Cache for generated name regular expressions. 32 | */ 33 | 34 | var REGEXP_CACHE = Object.create(null) 35 | 36 | /** 37 | * RegExp to match all characters to escape in a RegExp. 38 | */ 39 | 40 | var REGEXP_ESCAPE_CHARS_REGEXP = /[\^$\\.*+?()[\]{}|]/g 41 | 42 | /** 43 | * RegExp to match basic restricted name characters for loose validation. 44 | */ 45 | 46 | var RESTRICTED_NAME_CHARS_REGEXP = /[;=]/ 47 | 48 | /** 49 | * RegExp to match basic restricted value characters for loose validation. 50 | */ 51 | 52 | var RESTRICTED_VALUE_CHARS_REGEXP = /[;]/ 53 | 54 | /** 55 | * RegExp to match Same-Site cookie attribute value. 56 | */ 57 | 58 | var SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i 59 | 60 | function Cookies(request, response, options) { 61 | if (!(this instanceof Cookies)) return new Cookies(request, response, options) 62 | 63 | this.secure = undefined 64 | this.request = request 65 | this.response = response 66 | 67 | if (options) { 68 | if (Array.isArray(options)) { 69 | // array of key strings 70 | deprecate('"keys" argument; provide using options {"keys": [...]}') 71 | this.keys = new Keygrip(options) 72 | } else if (options.constructor && options.constructor.name === 'Keygrip') { 73 | // any keygrip constructor to allow different versions 74 | deprecate('"keys" argument; provide using options {"keys": keygrip}') 75 | this.keys = options 76 | } else { 77 | this.keys = Array.isArray(options.keys) ? new Keygrip(options.keys) : options.keys 78 | this.secure = options.secure 79 | } 80 | } 81 | } 82 | 83 | Cookies.prototype.get = function(name, opts) { 84 | var sigName = name + ".sig" 85 | , header, match, value, remote, data, index 86 | , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys 87 | 88 | header = this.request.headers["cookie"] 89 | if (!header) return 90 | 91 | match = header.match(getPattern(name)) 92 | if (!match) return 93 | 94 | value = match[1] 95 | if (value[0] === '"') value = value.slice(1, -1) 96 | if (!opts || !signed) return value 97 | 98 | remote = this.get(sigName) 99 | if (!remote) return 100 | 101 | data = name + "=" + value 102 | if (!this.keys) throw new Error('.keys required for signed cookies'); 103 | index = this.keys.index(data, remote) 104 | 105 | if (index < 0) { 106 | this.set(sigName, null, {path: "/", signed: false }) 107 | } else { 108 | index && this.set(sigName, this.keys.sign(data), { signed: false }) 109 | return value 110 | } 111 | }; 112 | 113 | Cookies.prototype.set = function(name, value, opts) { 114 | var res = this.response 115 | , req = this.request 116 | , headers = res.getHeader("Set-Cookie") || [] 117 | , cookie = new Cookie(name, value, opts) 118 | , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys 119 | var secure = this.secure === undefined 120 | ? req.protocol === 'https' || isRequestEncrypted(req) 121 | : Boolean(this.secure) 122 | 123 | if (typeof headers == "string") headers = [headers] 124 | 125 | if (!secure && opts && opts.secure) { 126 | throw new Error('Cannot send secure cookie over unencrypted connection') 127 | } 128 | 129 | cookie.secure = opts && opts.secure !== undefined 130 | ? opts.secure 131 | : secure 132 | 133 | if (opts && "secureProxy" in opts) { 134 | deprecate('"secureProxy" option; use "secure" option, provide "secure" to constructor if needed') 135 | cookie.secure = opts.secureProxy 136 | } 137 | 138 | pushCookie(headers, cookie) 139 | 140 | if (opts && signed) { 141 | if (!this.keys) throw new Error('.keys required for signed cookies'); 142 | cookie.value = this.keys.sign(cookie.toString()) 143 | cookie.name += ".sig" 144 | pushCookie(headers, cookie) 145 | } 146 | 147 | var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader 148 | setHeader.call(res, 'Set-Cookie', headers) 149 | return this 150 | }; 151 | 152 | function Cookie(name, value, attrs) { 153 | if (!fieldContentRegExp.test(name) || RESTRICTED_NAME_CHARS_REGEXP.test(name)) { 154 | throw new TypeError('argument name is invalid'); 155 | } 156 | 157 | if (value && (!fieldContentRegExp.test(value) || RESTRICTED_VALUE_CHARS_REGEXP.test(value))) { 158 | throw new TypeError('argument value is invalid'); 159 | } 160 | 161 | this.name = name 162 | this.value = value || "" 163 | 164 | for (var name in attrs) { 165 | this[name] = attrs[name] 166 | } 167 | 168 | if (!this.value) { 169 | this.expires = new Date(0) 170 | this.maxAge = null 171 | } 172 | 173 | if (this.path && !fieldContentRegExp.test(this.path)) { 174 | throw new TypeError('option path is invalid'); 175 | } 176 | 177 | if (this.domain && !fieldContentRegExp.test(this.domain)) { 178 | throw new TypeError('option domain is invalid'); 179 | } 180 | 181 | if (typeof this.maxAge === 'number' ? (isNaN(this.maxAge) || !isFinite(this.maxAge)) : this.maxAge) { 182 | throw new TypeError('option maxAge is invalid') 183 | } 184 | 185 | if (this.priority && !PRIORITY_REGEXP.test(this.priority)) { 186 | throw new TypeError('option priority is invalid') 187 | } 188 | 189 | if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) { 190 | throw new TypeError('option sameSite is invalid') 191 | } 192 | } 193 | 194 | Cookie.prototype.path = "/"; 195 | Cookie.prototype.expires = undefined; 196 | Cookie.prototype.domain = undefined; 197 | Cookie.prototype.httpOnly = true; 198 | Cookie.prototype.partitioned = false 199 | Cookie.prototype.priority = undefined 200 | Cookie.prototype.sameSite = false; 201 | Cookie.prototype.secure = false; 202 | Cookie.prototype.overwrite = false; 203 | 204 | Cookie.prototype.toString = function() { 205 | return this.name + "=" + this.value 206 | }; 207 | 208 | Cookie.prototype.toHeader = function() { 209 | var header = this.toString() 210 | 211 | if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge); 212 | 213 | if (this.path ) header += "; path=" + this.path 214 | if (this.expires ) header += "; expires=" + this.expires.toUTCString() 215 | if (this.domain ) header += "; domain=" + this.domain 216 | if (this.priority ) header += "; priority=" + this.priority.toLowerCase() 217 | if (this.sameSite ) header += "; samesite=" + (this.sameSite === true ? 'strict' : this.sameSite.toLowerCase()) 218 | if (this.secure ) header += "; secure" 219 | if (this.httpOnly ) header += "; httponly" 220 | if (this.partitioned) header += '; partitioned' 221 | 222 | return header 223 | }; 224 | 225 | // back-compat so maxage mirrors maxAge 226 | Object.defineProperty(Cookie.prototype, 'maxage', { 227 | configurable: true, 228 | enumerable: true, 229 | get: function () { return this.maxAge }, 230 | set: function (val) { return this.maxAge = val } 231 | }); 232 | deprecate.property(Cookie.prototype, 'maxage', '"maxage"; use "maxAge" instead') 233 | 234 | /** 235 | * Get the pattern to search for a cookie in a string. 236 | * @param {string} name 237 | * @private 238 | */ 239 | 240 | function getPattern (name) { 241 | if (!REGEXP_CACHE[name]) { 242 | REGEXP_CACHE[name] = new RegExp( 243 | '(?:^|;) *' + 244 | name.replace(REGEXP_ESCAPE_CHARS_REGEXP, '\\$&') + 245 | '=([^;]*)' 246 | ) 247 | } 248 | 249 | return REGEXP_CACHE[name] 250 | } 251 | 252 | /** 253 | * Get the encrypted status for a request. 254 | * 255 | * @param {object} req 256 | * @return {string} 257 | * @private 258 | */ 259 | 260 | function isRequestEncrypted (req) { 261 | return req.socket 262 | ? req.socket.encrypted 263 | : req.connection.encrypted 264 | } 265 | 266 | function pushCookie(headers, cookie) { 267 | if (cookie.overwrite) { 268 | for (var i = headers.length - 1; i >= 0; i--) { 269 | if (headers[i].indexOf(cookie.name + '=') === 0) { 270 | headers.splice(i, 1) 271 | } 272 | } 273 | } 274 | 275 | headers.push(cookie.toHeader()) 276 | } 277 | 278 | Cookies.connect = Cookies.express = function(keys) { 279 | return function(req, res, next) { 280 | req.cookies = res.cookies = new Cookies(req, res, { 281 | keys: keys 282 | }) 283 | 284 | next() 285 | } 286 | } 287 | 288 | Cookies.Cookie = Cookie 289 | 290 | module.exports = Cookies 291 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cookies", 3 | "description": "Cookies, optionally signed using Keygrip.", 4 | "version": "0.9.1", 5 | "author": "Jed Schmidt (http://jed.is)", 6 | "contributors": [ 7 | "Douglas Christopher Wilson " 8 | ], 9 | "license": "MIT", 10 | "repository": "pillarjs/cookies", 11 | "dependencies": { 12 | "depd": "~2.0.0", 13 | "keygrip": "~1.1.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "8.56.0", 17 | "express": "4.18.2", 18 | "mocha": "10.2.0", 19 | "nyc": "15.1.0", 20 | "restify": "8.6.1", 21 | "supertest": "6.3.3" 22 | }, 23 | "files": [ 24 | "HISTORY.md", 25 | "LICENSE", 26 | "README.md", 27 | "index.js" 28 | ], 29 | "engines": { 30 | "node": ">= 0.8" 31 | }, 32 | "scripts": { 33 | "lint": "eslint .", 34 | "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/", 35 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 36 | "test-cov": "nyc --reporter=html --reporter=text npm test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/cookie.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | var cookies = require('..') 4 | 5 | describe('new Cookie(name, value, [options])', function () { 6 | it('should have correct constructor', function () { 7 | var cookie = new cookies.Cookie('foo', 'bar') 8 | assert.equal(cookie.constructor, cookies.Cookie) 9 | }) 10 | 11 | it('should throw on invalid name', function () { 12 | assert.throws(function () { 13 | new cookies.Cookie('foo\n', 'bar') 14 | }, /argument name is invalid/) 15 | assert.throws(function () { 16 | assert.ok(!new cookies.Cookie('foo=', 'bar')) 17 | }, /argument name is invalid/) 18 | }) 19 | 20 | it('should throw on invalid value', function () { 21 | assert.throws(function () { 22 | new cookies.Cookie('foo', 'bar\n') 23 | }, /argument value is invalid/) 24 | assert.throws(function () { 25 | assert.ok(!new cookies.Cookie('foo', 'bar;')) 26 | }, /argument value is invalid/) 27 | }) 28 | 29 | it('should throw on invalid path', function () { 30 | assert.throws(function () { 31 | new cookies.Cookie('foo', 'bar', { path: '/\n' }) 32 | }, /option path is invalid/) 33 | }) 34 | 35 | it('should throw on invalid domain', function () { 36 | assert.throws(function () { 37 | new cookies.Cookie('foo', 'bar', { domain: 'example.com\n' }) 38 | }, /option domain is invalid/) 39 | }) 40 | 41 | describe('options', function () { 42 | describe('maxage', function () { 43 | it('should set the .maxAge property', function () { 44 | var cookie = new cookies.Cookie('foo', 'bar', { maxage: 86400 }) 45 | assert.equal(cookie.maxAge, 86400) 46 | }) 47 | 48 | it('should set the .maxage property', function () { 49 | var cookie = new cookies.Cookie('foo', 'bar', { maxage: 86400 }) 50 | assert.equal(cookie.maxage, 86400) 51 | }) 52 | }) 53 | 54 | describe('maxAge', function () { 55 | it('should set the .maxAge property', function () { 56 | var cookie = new cookies.Cookie('foo', 'bar', { maxAge: 86400 }) 57 | assert.equal(cookie.maxAge, 86400) 58 | }) 59 | 60 | it('should set the .maxage property', function () { 61 | var cookie = new cookies.Cookie('foo', 'bar', { maxAge: 86400 }) 62 | assert.equal(cookie.maxage, 86400) 63 | }) 64 | 65 | it('should throw on invalid value', function () { 66 | assert.throws(function () { 67 | new cookies.Cookie('foo', 'bar', { maxAge: 'foo' }) 68 | }, /option maxAge is invalid/) 69 | }) 70 | 71 | it('should throw on Infinity', function () { 72 | assert.throws(function () { 73 | new cookies.Cookie('foo', 'bar', { maxAge: Infinity }) 74 | }, /option maxAge is invalid/) 75 | }) 76 | 77 | it('should throw on NaN', function () { 78 | assert.throws(function () { 79 | new cookies.Cookie('foo', 'bar', { maxAge: NaN }) 80 | }, /option maxAge is invalid/) 81 | }) 82 | }) 83 | 84 | describe('partitioned', function () { 85 | it('should set the .partitioned property', function () { 86 | var cookie = new cookies.Cookie('foo', 'bar', { partitioned: true }) 87 | assert.strictEqual(cookie.partitioned, true) 88 | }) 89 | 90 | it('should default to false', function () { 91 | var cookie = new cookies.Cookie('foo', 'bar') 92 | assert.strictEqual(cookie.partitioned, false) 93 | }) 94 | 95 | describe('when set to false', function () { 96 | it('should not set partitioned attribute in header', function () { 97 | var cookie = new cookies.Cookie('foo', 'bar', { partitioned: false }) 98 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; httponly') 99 | }) 100 | }) 101 | 102 | describe('when set to true', function () { 103 | it('should set partitioned attribute in header', function () { 104 | var cookie = new cookies.Cookie('foo', 'bar', { partitioned: true }) 105 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; httponly; partitioned') 106 | }) 107 | }) 108 | }) 109 | 110 | describe('priority', function () { 111 | it('should set the .priority property', function () { 112 | var cookie = new cookies.Cookie('foo', 'bar', { priority: 'low' }) 113 | assert.strictEqual(cookie.priority, 'low') 114 | }) 115 | 116 | it('should default to undefined', function () { 117 | var cookie = new cookies.Cookie('foo', 'bar') 118 | assert.strictEqual(cookie.priority, undefined) 119 | }) 120 | 121 | it('should throw on invalid value', function () { 122 | assert.throws(function () { 123 | new cookies.Cookie('foo', 'bar', { priority: 'foo' }) 124 | }, /option priority is invalid/) 125 | }) 126 | 127 | describe('when set to "low"', function () { 128 | it('should set "priority=low" attribute in header', function () { 129 | var cookie = new cookies.Cookie('foo', 'bar', { priority: 'low' }) 130 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; priority=low; httponly') 131 | }) 132 | }) 133 | 134 | describe('when set to "medium"', function () { 135 | it('should set "priority=medium" attribute in header', function () { 136 | var cookie = new cookies.Cookie('foo', 'bar', { priority: 'medium' }) 137 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; priority=medium; httponly') 138 | }) 139 | }) 140 | 141 | describe('when set to "high"', function () { 142 | it('should set "priority=high" attribute in header', function () { 143 | var cookie = new cookies.Cookie('foo', 'bar', { priority: 'high' }) 144 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; priority=high; httponly') 145 | }) 146 | }) 147 | 148 | describe('when set to "HIGH"', function () { 149 | it('should set "priority=high" attribute in header', function () { 150 | var cookie = new cookies.Cookie('foo', 'bar', { priority: 'HIGH' }) 151 | assert.strictEqual(cookie.toHeader(), 'foo=bar; path=/; priority=high; httponly') 152 | }) 153 | }) 154 | }) 155 | 156 | describe('sameSite', function () { 157 | it('should set the .sameSite property', function () { 158 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: true }) 159 | assert.equal(cookie.sameSite, true) 160 | }) 161 | 162 | it('should default to false', function () { 163 | var cookie = new cookies.Cookie('foo', 'bar') 164 | assert.equal(cookie.sameSite, false) 165 | }) 166 | 167 | it('should throw on invalid value', function () { 168 | assert.throws(function () { 169 | new cookies.Cookie('foo', 'bar', { sameSite: 'foo' }) 170 | }, /option sameSite is invalid/) 171 | }) 172 | 173 | describe('when set to "false"', function () { 174 | it('should not set "samesite" attribute in header', function () { 175 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: false }) 176 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; httponly') 177 | }) 178 | }) 179 | 180 | describe('when set to "true"', function () { 181 | it('should set "samesite=strict" attribute in header', function () { 182 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: true }) 183 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; samesite=strict; httponly') 184 | }) 185 | }) 186 | 187 | describe('when set to "lax"', function () { 188 | it('should set "samesite=lax" attribute in header', function () { 189 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: 'lax' }) 190 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; samesite=lax; httponly') 191 | }) 192 | }) 193 | 194 | describe('when set to "none"', function () { 195 | it('should set "samesite=none" attribute in header', function () { 196 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: 'none' }) 197 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; samesite=none; httponly') 198 | }) 199 | }) 200 | 201 | describe('when set to "strict"', function () { 202 | it('should set "samesite=strict" attribute in header', function () { 203 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: 'strict' }) 204 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; samesite=strict; httponly') 205 | }) 206 | }) 207 | 208 | describe('when set to "STRICT"', function () { 209 | it('should set "samesite=strict" attribute in header', function () { 210 | var cookie = new cookies.Cookie('foo', 'bar', { sameSite: 'STRICT' }) 211 | assert.equal(cookie.toHeader(), 'foo=bar; path=/; samesite=strict; httponly') 212 | }) 213 | }) 214 | }) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /test/express.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require( "assert" ) 3 | , keys = require( "keygrip" )(['a', 'b']) 4 | , cookies = require( "../" ).express 5 | , request = require('supertest') 6 | 7 | var express = tryRequire('express') 8 | 9 | var describeExpress = express ? describe : describe.skip 10 | 11 | describeExpress('Express', function () { 12 | it('should set a cookie on the response', function (done) { 13 | var app = express() 14 | 15 | app.set('env', 'test') 16 | app.use(cookies()) 17 | app.get('/', function (req, res) { 18 | res.cookies.set('foo', 'bar') 19 | res.end() 20 | }) 21 | 22 | request(app) 23 | .get('/') 24 | .expect(shouldSetCookies([ 25 | { name: 'foo', value: 'bar', path: '/', httponly: true } 26 | ])) 27 | .expect(200, done) 28 | }) 29 | 30 | it('should get a cookie from the request', function (done) { 31 | var app = express() 32 | 33 | app.set('env', 'test') 34 | app.use(cookies()) 35 | app.get('/', function (req, res) { 36 | res.json({ foo: String(res.cookies.get('foo')) }) 37 | }) 38 | 39 | request(app) 40 | .get('/') 41 | .set('cookie', 'foo=bar') 42 | .expect(200, { foo: 'bar' }, done) 43 | }) 44 | 45 | describe('with multiple cookies', function () { 46 | it('should set all cookies on the response', function (done) { 47 | var app = express() 48 | 49 | app.set('env', 'test') 50 | app.use(cookies()) 51 | app.get('/', function (req, res) { 52 | res.cookies.set('foo', 'bar') 53 | res.cookies.set('fizz', 'buzz') 54 | res.end() 55 | }) 56 | 57 | request(app) 58 | .get('/') 59 | .expect(shouldSetCookies([ 60 | { name: 'foo', value: 'bar', path: '/', httponly: true }, 61 | { name: 'fizz', value: 'buzz', path: '/', httponly: true } 62 | ])) 63 | .expect(200, done) 64 | }) 65 | 66 | it('should get each cookie from the request', function (done) { 67 | var app = express() 68 | 69 | app.set('env', 'test') 70 | app.use(cookies()) 71 | app.get('/', function (req, res) { 72 | res.json({ 73 | fizz: String(res.cookies.get('fizz')), 74 | foo: String(res.cookies.get('foo')) 75 | }) 76 | }) 77 | 78 | request(app) 79 | .get('/') 80 | .set('cookie', 'foo=bar; fizz=buzz') 81 | .expect(200, { foo: 'bar', fizz: 'buzz' }, done) 82 | }) 83 | }) 84 | 85 | describe('when "overwrite: false"', function () { 86 | it('should set second cookie with same name', function (done) { 87 | var app = express() 88 | 89 | app.set('env', 'test') 90 | app.use(cookies()) 91 | app.get('/', function (req, res) { 92 | res.cookies.set('foo', 'bar') 93 | res.cookies.set('foo', 'fizz', { overwrite: false }) 94 | res.end() 95 | }) 96 | 97 | request(app) 98 | .get('/') 99 | .expect(shouldSetCookies([ 100 | { name: 'foo', value: 'bar', path: '/', httponly: true }, 101 | { name: 'foo', value: 'fizz', path: '/', httponly: true } 102 | ])) 103 | .expect(200, done) 104 | }) 105 | }) 106 | 107 | describe('when "overwrite: true"', function () { 108 | it('should replace previously set value', function (done) { 109 | var app = express() 110 | 111 | app.set('env', 'test') 112 | app.use(cookies()) 113 | app.get('/', function (req, res, next) { 114 | res.cookies.set('foo', 'bar') 115 | res.cookies.set('foo', 'fizz', { overwrite: true }) 116 | res.end() 117 | }) 118 | 119 | request(app) 120 | .get('/') 121 | .expect(shouldSetCookies([ 122 | { name: 'foo', value: 'fizz', path: '/', httponly: true } 123 | ])) 124 | .expect(200, done) 125 | }) 126 | 127 | it('should set signature correctly', function (done) { 128 | var app = express() 129 | 130 | app.set('env', 'test') 131 | app.use(cookies(keys)) 132 | app.get('/', function (req, res, next) { 133 | res.cookies.set('foo', 'bar') 134 | res.cookies.set('foo', 'fizz', { overwrite: true }) 135 | res.end() 136 | }) 137 | 138 | request(app) 139 | .get('/') 140 | .expect(shouldSetCookies([ 141 | { name: 'foo', value: 'fizz', path: '/', httponly: true }, 142 | { name: 'foo.sig', value: 'hVIYdxZSelh3gIK5wQxzrqoIndU', path: '/', httponly: true } 143 | ])) 144 | .expect(200, done) 145 | }) 146 | }) 147 | 148 | describe('when "secure: true"', function () { 149 | it('should not set when not secure', function (done) { 150 | var app = express() 151 | 152 | app.set('env', 'test') 153 | app.use(cookies(keys)) 154 | app.use(function (req, res) { 155 | res.cookies.set('foo', 'bar', {secure: true}) 156 | res.end() 157 | }) 158 | 159 | request(app) 160 | .get('/') 161 | .expect(500, /Cannot send secure cookie over unencrypted connection/, done) 162 | }) 163 | 164 | it('should set for secure connection', function (done) { 165 | var app = express() 166 | 167 | app.set('env', 'test') 168 | app.use(cookies(keys)) 169 | app.use(function (req, res, next) { 170 | res.connection.encrypted = true 171 | next() 172 | }) 173 | app.use(function (req, res) { 174 | res.cookies.set('foo', 'bar', {secure: true}) 175 | res.end() 176 | }) 177 | 178 | request(app) 179 | .get('/') 180 | .expect(shouldSetCookies([ 181 | { name: 'foo', value: 'bar', path: '/', httponly: true, secure: true }, 182 | { name: 'foo.sig', value: 'p5QVCZeqNBulWOhYipO0jqjrzz4', path: '/', httponly: true, secure: true } 183 | ])) 184 | .expect(200, done) 185 | }) 186 | 187 | it('should set for proxy settings', function (done) { 188 | var app = express() 189 | 190 | app.set('env', 'test') 191 | app.set('trust proxy', true) 192 | app.use(cookies(keys)) 193 | app.use(function (req, res) { 194 | res.cookies.set('foo', 'bar', {secure: true}) 195 | res.end() 196 | }) 197 | 198 | request(app) 199 | .get('/') 200 | .set('X-Forwarded-Proto', 'https') 201 | .expect(shouldSetCookies([ 202 | { name: 'foo', value: 'bar', path: '/', httponly: true, secure: true }, 203 | { name: 'foo.sig', value: 'p5QVCZeqNBulWOhYipO0jqjrzz4', path: '/', httponly: true, secure: true } 204 | ])) 205 | .expect(200, done) 206 | }) 207 | }) 208 | }) 209 | 210 | function getCookies (res) { 211 | var setCookies = res.headers['set-cookie'] || [] 212 | return setCookies.map(parseSetCookie) 213 | } 214 | 215 | function parseSetCookie (header) { 216 | var match 217 | var pairs = [] 218 | var pattern = /\s*([^=;]+)(?:=([^;]*);?|;|$)/g 219 | 220 | while ((match = pattern.exec(header))) { 221 | pairs.push({ name: match[1], value: match[2] }) 222 | } 223 | 224 | var cookie = pairs.shift() 225 | 226 | for (var i = 0; i < pairs.length; i++) { 227 | match = pairs[i] 228 | cookie[match.name.toLowerCase()] = (match.value || true) 229 | } 230 | 231 | return cookie 232 | } 233 | 234 | function shouldSetCookies (expected) { 235 | return function (res) { 236 | assert.deepEqual(getCookies(res), expected) 237 | } 238 | } 239 | 240 | function tryRequire (name) { 241 | try { 242 | return require(name) 243 | } catch (e) { 244 | return undefined 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /test/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwDCCAaigAwIBAgIJAM6yBH2RuaRYMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMTCWxvY2FsaG9zdDAgFw0xODExMDQyMzMyMDBaGA8yMDY4MTAyMjIzMzIwMFow 4 | FDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAurkioeD9fi3tDt80CNr0sWxQHrZaugzjIAV91194m1fslhD4PaxYOn1A 6 | rPS1ntx7KnBU4drf1e/0WOuv7UJki84ic4mzmIQaZGvTyJsop9EpOc+1NNli6BVe 7 | hU/zd/5Qp9JCjQmHt+fTbEwxdAEL77f1OdHRnyDo7VexYjReCFuFaQm/Yp60BOmi 8 | i5mCBuof6rBwmxD/lTQUoQOG3APwna7udBxC7FUSvKVv1tY6QzhrSedRkDW6J3Gr 9 | ylDzG+1qmEfNudWEU+WFlL0syDfrxWM4kdaOotYdeo2SztrDzRwwnZ6DtyqH/Pcp 10 | jlUGZS5thb0G0tJcjQoDHoybmPTnRwIDAQABoxMwETAPBgNVHREECDAGhwR/AAAB 11 | MA0GCSqGSIb3DQEBCwUAA4IBAQAwzoLRl2U1i3AsBE7wgOWzxY81XmrN1ODJdFOl 12 | a9x0DH2K1v5vueU+1CWYgCRKhNVPUHXT6FWFwG0WkvjvLBiSfD/CeyYDN3DSRovg 13 | AhOQfnQh3Owdm/D0vDb3LqWBdcIoFAWPAarkxClohMfqd3+8XojEIeMm+mE6dwHc 14 | mP6kxKTtUcuE8OSOc/re4r1VEeX9MzPkHF2HAzWMsXF56HS5fMRdfyZuIlpTavwi 15 | R9IEM74l58d0LSC2Qa5oK+McBE7wX/xCjdiuQNoapq4WcG9vX2VuJnehv6a+iBLG 16 | 06PP7sLHbPq0mHJKmnZfZWFl+fnYEhSSV1OycoICiSAzUQ/k 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6uSKh4P1+Le0O 3 | 3zQI2vSxbFAetlq6DOMgBX3XX3ibV+yWEPg9rFg6fUCs9LWe3HsqcFTh2t/V7/RY 4 | 66/tQmSLziJzibOYhBpka9PImyin0Sk5z7U02WLoFV6FT/N3/lCn0kKNCYe359Ns 5 | TDF0AQvvt/U50dGfIOjtV7FiNF4IW4VpCb9inrQE6aKLmYIG6h/qsHCbEP+VNBSh 6 | A4bcA/Cdru50HELsVRK8pW/W1jpDOGtJ51GQNboncavKUPMb7WqYR8251YRT5YWU 7 | vSzIN+vFYziR1o6i1h16jZLO2sPNHDCdnoO3Kof89ymOVQZlLm2FvQbS0lyNCgMe 8 | jJuY9OdHAgMBAAECggEAQC3uR3HL75jdiGUTv49y16EBEO1g1d4kGxeIH4UDMXHR 9 | Met3R8t7L+9pUYly+72Q4A9oPZE7qo8lj4LDH2vYn20kzk2gW2XcpHOjgapDGRt9 10 | bg+Emzu1EUx3Bp9qce4JzwUoNs31xjJ6qxitTtAlSCoUseD6ihWHujyQDc8uGvSz 11 | zkGNQxDdEC5JMDMNWp/0syDPqqNynUcGbvanwJP5MqJwVDRfRA087iZcwlqMQW0X 12 | C0EJQ1h5xulJz327lRjSzgAxmYlVOMrQRMhAniiN90Y4FNxMDNWb+RsxQMeGo4fv 13 | zUDbT9s/Dk6u29Ts982Yd973M6uW4ukMvltN0AHHQQKBgQDiHzA5W5QKAXQvpuRN 14 | T19QKRxuoCRIm2VvXgd4ZSJoMi8+gshmj0+H440wMXa1pWbFIHsbUn7ua1V4NzIP 15 | 5STnInswXfOjezSDBe6BtCe6Fr1PPR8MAcqvS/hrHPaKOUX4m4b6Okff0JYAHbr0 16 | AXTVP6hUujAV1EBud6T5XcOikQKBgQDTZT7spKr46vD3iTg+z/vrkILcXpR3JMra 17 | YVDZ8hl+tSVT0EVvB9lp5cNSWBQkCj6+i+KUhJBwUxa3Kx3aP81bIQfIl9TkKdt6 18 | nlkkVZ+6Zp3rBfpwC+BNwwmAkRWTqhUM1OveH6S7YnpscKCEkr7XmobOqONAwtWh 19 | kacDLfIoVwKBgEJz3/w7SZpXKwoGBfoiZWRtcImiTod4A6ti+tcLAb2VYgUA8lwR 20 | qdHJseiD4NspLLaqAQPajqsKqCeYMQIy2VGD9KgWNE/LGXeX+qvrgfFSVXhAAivJ 21 | KwOxU+RGsr2Ub1fMfTJ0hkLkTfDiy9qBwxAYkSO1RARmSDkuuDEAuUnBAoGBAJdx 22 | AuE5HprwhOxwy3CEQ2+AuZ7xyt6H5yMHcIqSXB1f3HvsyrE+KE0rIwCMxPEEvep4 23 | ADxxs6AkhjN2mg5Ogul0AkV8MDG8otV3N1lGVgWNmjhSshUvDOPog5gtWA3PXQEy 24 | UD9y3+q2JAonrXcMQdfnhgfrCdLNQYpb9A/dDIxFAoGAaeMsz3naEEDUFvPEUV63 25 | Bc/dnbD/SavZBZ8oldkw75HdnNqtaYEauvW4uaQ9oYQpkSQ+X07eBdduiBvfkh8m 26 | Uhv4M/K2u78EJz0ie+ez6wKNI5oAKzY5KtXioKQ1Mu+DZgYU9m06B6HSI2rSzHiT 27 | 5viM6XkrV2KXrNFOH6/z+ak= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/restify.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , keys = require('keygrip')(['a', 'b']) 3 | , Cookies = require('../') 4 | , request = require('supertest') 5 | 6 | var restify = tryRequire('restify') 7 | 8 | var describeRestify = restify ? describe : describe.skip 9 | 10 | describeRestify('Restify', function () { 11 | it('should set a cookie on the response', function (done) { 12 | var server = restify.createServer() 13 | 14 | server.get('/', function (req, res) { 15 | var cookies = new Cookies(req, res) 16 | 17 | cookies.set('foo', 'bar') 18 | 19 | res.send(200) 20 | }) 21 | 22 | request(server) 23 | .get('/') 24 | .expect(shouldSetCookies([ 25 | { name: 'foo', value: 'bar', path: '/', httponly: true } 26 | ])) 27 | .expect(200, done) 28 | }) 29 | 30 | it('should get a cookie from the request', function (done) { 31 | var server = restify.createServer() 32 | 33 | server.get('/', function (req, res) { 34 | var cookies = new Cookies(req, res) 35 | 36 | res.send({ foo: String(cookies.get('foo')) }) 37 | }) 38 | 39 | request(server) 40 | .get('/') 41 | .set('cookie', 'foo=bar') 42 | .expect(200, { foo: 'bar' }, done) 43 | }) 44 | 45 | describe('with multiple cookies', function () { 46 | it('should set all cookies on the response', function (done) { 47 | var server = restify.createServer() 48 | 49 | server.get('/', function (req, res) { 50 | var cookies = new Cookies(req, res) 51 | 52 | cookies.set('foo', 'bar') 53 | cookies.set('fizz', 'buzz') 54 | 55 | res.send(200) 56 | }) 57 | 58 | request(server) 59 | .get('/') 60 | .expect(shouldSetCookies([ 61 | { name: 'foo', value: 'bar', path: '/', httponly: true }, 62 | { name: 'fizz', value: 'buzz', path: '/', httponly: true } 63 | ])) 64 | .expect(200, done) 65 | }) 66 | 67 | it('should get each cookie from the request', function (done) { 68 | var server = restify.createServer() 69 | 70 | server.get('/', function (req, res) { 71 | var cookies = new Cookies(req, res) 72 | 73 | res.send({ 74 | fizz: String(cookies.get('fizz')), 75 | foo: String(cookies.get('foo')) 76 | }) 77 | }) 78 | 79 | request(server) 80 | .get('/') 81 | .set('cookie', 'foo=bar; fizz=buzz') 82 | .expect(200, { foo: 'bar', fizz: 'buzz' }, done) 83 | }) 84 | }) 85 | 86 | describe('when "overwrite: false"', function () { 87 | it('should set second cookie with same name', function (done) { 88 | var server = restify.createServer() 89 | 90 | server.get('/', function (req, res) { 91 | var cookies = new Cookies(req, res) 92 | 93 | cookies.set('foo', 'bar') 94 | cookies.set('foo', 'fizz', { overwrite: false }) 95 | 96 | res.send(200) 97 | }) 98 | 99 | request(server) 100 | .get('/') 101 | .expect(shouldSetCookies([ 102 | { name: 'foo', value: 'bar', path: '/', httponly: true }, 103 | { name: 'foo', value: 'fizz', path: '/', httponly: true } 104 | ])) 105 | .expect(200, done) 106 | }) 107 | }) 108 | 109 | describe('when "overwrite: true"', function () { 110 | it('should replace previously set value', function (done) { 111 | var server = restify.createServer() 112 | 113 | server.get('/', function (req, res) { 114 | var cookies = new Cookies(req, res) 115 | 116 | cookies.set('foo', 'bar') 117 | cookies.set('foo', 'fizz', { overwrite: true }) 118 | 119 | res.send(200) 120 | }) 121 | 122 | request(server) 123 | .get('/') 124 | .expect(shouldSetCookies([ 125 | { name: 'foo', value: 'fizz', path: '/', httponly: true } 126 | ])) 127 | .expect(200, done) 128 | }) 129 | 130 | it('should set signature correctly', function (done) { 131 | var server = restify.createServer() 132 | 133 | server.get('/', function (req, res) { 134 | var cookies = new Cookies(req, res, keys) 135 | 136 | cookies.set('foo', 'bar') 137 | cookies.set('foo', 'fizz', { overwrite: true }) 138 | 139 | res.send(200) 140 | }) 141 | 142 | request(server) 143 | .get('/') 144 | .expect(shouldSetCookies([ 145 | { name: 'foo', value: 'fizz', path: '/', httponly: true }, 146 | { name: 'foo.sig', value: 'hVIYdxZSelh3gIK5wQxzrqoIndU', path: '/', httponly: true } 147 | ])) 148 | .expect(200, done) 149 | }) 150 | }) 151 | }) 152 | 153 | function getCookies (res) { 154 | var setCookies = res.headers['set-cookie'] || [] 155 | return setCookies.map(parseSetCookie) 156 | } 157 | 158 | function parseSetCookie (header) { 159 | var match 160 | var pairs = [] 161 | var pattern = /\s*([^=;]+)(?:=([^;]*);?|;|$)/g 162 | 163 | while ((match = pattern.exec(header))) { 164 | pairs.push({ name: match[1], value: match[2] }) 165 | } 166 | 167 | var cookie = pairs.shift() 168 | 169 | for (var i = 0; i < pairs.length; i++) { 170 | match = pairs[i] 171 | cookie[match.name.toLowerCase()] = (match.value || true) 172 | } 173 | 174 | return cookie 175 | } 176 | 177 | function shouldSetCookies (expected) { 178 | return function (res) { 179 | assert.deepEqual(getCookies(res), expected) 180 | } 181 | } 182 | 183 | function tryRequire (name) { 184 | try { 185 | return require(name) 186 | } catch (e) { 187 | return undefined 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/support/env.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test' 2 | process.env.NO_DEPRECATION = 'cookies' 3 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | var Cookies = require('..') 4 | var fs = require('fs') 5 | var http = require('http') 6 | var https = require('https') 7 | var Keygrip = require('keygrip') 8 | var path = require('path') 9 | var request = require('supertest') 10 | 11 | describe('new Cookies(req, res, [options])', function () { 12 | it('should create new cookies instance', function (done) { 13 | assertServer(done, function (req, res) { 14 | var cookies = new Cookies(req, res) 15 | assert.ok(cookies) 16 | assert.strictEqual(cookies.constructor, Cookies) 17 | assert.strictEqual(cookies.request, req) 18 | assert.strictEqual(cookies.response, res) 19 | assert.strictEqual(cookies.keys, undefined) 20 | }) 21 | }) 22 | 23 | describe('options', function () { 24 | it('should accept array of keys', function (done) { 25 | assertServer(done, function (req, res) { 26 | var cookies = new Cookies(req, res, ['keyboard cat']) 27 | assert.strictEqual(typeof cookies.keys, 'object') 28 | assert.strictEqual(cookies.keys.sign('foo=bar'), 'iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 29 | }) 30 | }) 31 | 32 | it('should accept Keygrip instance', function (done) { 33 | assertServer(done, function (req, res) { 34 | var keys = new Keygrip(['keyboard cat']) 35 | var cookies = new Cookies(req, res, keys) 36 | assert.strictEqual(typeof cookies.keys, 'object') 37 | assert.strictEqual(cookies.keys.sign('foo=bar'), 'iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 38 | }) 39 | }) 40 | 41 | describe('.keys', function () { 42 | it('should accept array of keys', function (done) { 43 | assertServer(done, function (req, res) { 44 | var cookies = new Cookies(req, res, { keys: ['keyboard cat'] }) 45 | assert.strictEqual(typeof cookies.keys, 'object') 46 | assert.strictEqual(cookies.keys.sign('foo=bar'), 'iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 47 | }) 48 | }) 49 | 50 | it('should accept Keygrip instance', function (done) { 51 | assertServer(done, function (req, res) { 52 | var keys = new Keygrip(['keyboard cat']) 53 | var cookies = new Cookies(req, res, { keys: keys }) 54 | assert.strictEqual(typeof cookies.keys, 'object') 55 | assert.strictEqual(cookies.keys.sign('foo=bar'), 'iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 56 | }) 57 | }) 58 | }) 59 | 60 | describe('.secure', function () { 61 | it('should default to undefined', function (done) { 62 | assertServer(done, function (req, res) { 63 | var cookies = new Cookies(req, res) 64 | assert.strictEqual(cookies.secure, undefined) 65 | }) 66 | }) 67 | 68 | it('should set secure flag', function (done) { 69 | assertServer(done, function (req, res) { 70 | var cookies = new Cookies(req, res, { secure: true }) 71 | assert.strictEqual(cookies.secure, true) 72 | }) 73 | }) 74 | }) 75 | }) 76 | 77 | describe('.get(name, [options])', function () { 78 | it('should return value of cookie', function (done) { 79 | request(createServer(getCookieHandler('foo'))) 80 | .get('/') 81 | .set('Cookie', 'foo=bar') 82 | .expect(200, 'bar', done) 83 | }) 84 | 85 | it('should return unquoted value', function (done) { 86 | request(createServer(getCookieHandler('foo'))) 87 | .get('/') 88 | .set('Cookie', 'foo="bar"') 89 | .expect(200, 'bar', done) 90 | }) 91 | 92 | it('should work for cookie name with special characters', function (done) { 93 | request(createServer(getCookieHandler('foo*(#bar)?.|$'))) 94 | .get('/') 95 | .set('Cookie', 'foo*(#bar)?.|$=buzz') 96 | .expect(200, 'buzz', done) 97 | }) 98 | 99 | it('should return undefined without cookie', function (done) { 100 | request(createServer(getCookieHandler('fizz'))) 101 | .get('/') 102 | .set('Cookie', 'foo=bar') 103 | .expect(200, 'undefined', done) 104 | }) 105 | 106 | it('should return undefined without header', function (done) { 107 | request(createServer(getCookieHandler('foo'))) 108 | .get('/') 109 | .expect(200, 'undefined', done) 110 | }) 111 | 112 | describe('"signed" option', function () { 113 | describe('when true', function () { 114 | it('should throw without .keys', function (done) { 115 | request(createServer(getCookieHandler('foo', { signed: true }))) 116 | .get('/') 117 | .set('Cookie', 'foo=bar; foo.sig=iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 118 | .expect(500) 119 | .expect('Error: .keys required for signed cookies') 120 | .end(done) 121 | }) 122 | 123 | it('should return signed cookie value', function (done) { 124 | var opts = { keys: ['keyboard cat'] } 125 | request(createServer(opts, getCookieHandler('foo', { signed: true }))) 126 | .get('/') 127 | .set('Cookie', 'foo=bar; foo.sig=iW2fuCIzk9Cg_rqLT1CAqrtdWs8') 128 | .expect(200, 'bar', done) 129 | }) 130 | 131 | describe('when signature is invalid', function () { 132 | it('should return undefined', function (done) { 133 | var opts = { keys: ['keyboard cat'] } 134 | request(createServer(opts, getCookieHandler('foo', { signed: true }))) 135 | .get('/') 136 | .set('Cookie', 'foo=bar; foo.sig=v5f380JakwVgx2H9B9nA6kJaZNg') 137 | .expect(200, 'undefined', done) 138 | }) 139 | 140 | it('should delete signature cookie', function (done) { 141 | var opts = { keys: ['keyboard cat'] } 142 | request(createServer(opts, getCookieHandler('foo', { signed: true }))) 143 | .get('/') 144 | .set('Cookie', 'foo=bar; foo.sig=v5f380JakwVgx2H9B9nA6kJaZNg') 145 | .expect(200) 146 | .expect('undefined') 147 | .expect(shouldSetCookieCount(1)) 148 | .expect(shouldSetCookieWithAttributeAndValue('foo.sig', 'expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) 149 | .end(done) 150 | }) 151 | }) 152 | 153 | describe('when signature matches old key', function () { 154 | it('should return signed value', function (done) { 155 | var opts = { keys: ['keyboard cat a', 'keyboard cat b'] } 156 | request(createServer(opts, getCookieHandler('foo', { signed: true }))) 157 | .get('/') 158 | .set('Cookie', 'foo=bar; foo.sig=NzdRHeORj7MtAMhSsILYRsyVNI8') 159 | .expect(200, 'bar', done) 160 | }) 161 | 162 | it('should set signature with new key', function (done) { 163 | var opts = { keys: ['keyboard cat a', 'keyboard cat b'] } 164 | request(createServer(opts, getCookieHandler('foo', { signed: true }))) 165 | .get('/') 166 | .set('Cookie', 'foo=bar; foo.sig=NzdRHeORj7MtAMhSsILYRsyVNI8') 167 | .expect(200) 168 | .expect('bar') 169 | .expect(shouldSetCookieCount(1)) 170 | .expect(shouldSetCookieToValue('foo.sig', 'tecF04p5ua6TnfYxUTDskgWSKJE')) 171 | .end(done) 172 | }) 173 | }) 174 | }) 175 | }) 176 | }) 177 | 178 | describe('.set(name, value, [options])', function () { 179 | it('should set cookie', function (done) { 180 | request(createServer(setCookieHandler('foo', 'bar'))) 181 | .get('/') 182 | .expect(200) 183 | .expect(shouldSetCookieToValue('foo', 'bar')) 184 | .end(done) 185 | }) 186 | 187 | it('should work for cookie name with special characters', function (done) { 188 | request(createServer(setCookieHandler('foo*(#bar)?.|$', 'buzz'))) 189 | .get('/') 190 | .expect(200) 191 | .expect(shouldSetCookieToValue('foo*(#bar)?.|$', 'buzz')) 192 | .end(done) 193 | }) 194 | 195 | it('should work for cookie value with special characters', function (done) { 196 | request(createServer(setCookieHandler('foo', '*(#bar)?.|$'))) 197 | .get('/') 198 | .expect(200) 199 | .expect(shouldSetCookieToValue('foo', '*(#bar)?.|$')) 200 | .end(done) 201 | }) 202 | 203 | describe('when value is falsy', function () { 204 | it('should delete cookie', function (done) { 205 | request(createServer(setCookieHandler('foo', null))) 206 | .get('/') 207 | .expect(200) 208 | .expect(shouldSetCookieCount(1)) 209 | .expect(shouldSetCookieToValue('foo', '')) 210 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) 211 | .end(done) 212 | }) 213 | }) 214 | 215 | describe('"httpOnly" option', function () { 216 | it('should be set by default', function (done) { 217 | request(createServer(setCookieHandler('foo', 'bar'))) 218 | .get('/') 219 | .expect(200) 220 | .expect(shouldSetCookieWithAttribute('foo', 'httpOnly')) 221 | .end(done) 222 | }) 223 | 224 | it('should set to true', function (done) { 225 | request(createServer(setCookieHandler('foo', 'bar', { httpOnly: true }))) 226 | .get('/') 227 | .expect(200) 228 | .expect(shouldSetCookieWithAttribute('foo', 'httpOnly')) 229 | .end(done) 230 | }) 231 | 232 | it('should set to false', function (done) { 233 | request(createServer(setCookieHandler('foo', 'bar', { httpOnly: false }))) 234 | .get('/') 235 | .expect(200) 236 | .expect(shouldSetCookieWithoutAttribute('foo', 'httpOnly')) 237 | .end(done) 238 | }) 239 | }) 240 | 241 | describe('"domain" option', function () { 242 | it('should not be set by default', function (done) { 243 | request(createServer(setCookieHandler('foo', 'bar'))) 244 | .get('/') 245 | .expect(200) 246 | .expect(shouldSetCookieWithoutAttribute('foo', 'domain')) 247 | .end(done) 248 | }) 249 | 250 | it('should set to custom value', function (done) { 251 | request(createServer(setCookieHandler('foo', 'bar', { domain: 'foo.local' }))) 252 | .get('/') 253 | .expect(200) 254 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'domain', 'foo.local')) 255 | .end(done) 256 | }) 257 | 258 | it('should reject invalid value', function (done) { 259 | request(createServer(setCookieHandler('foo', 'bar', { domain: 'foo\nbar' }))) 260 | .get('/') 261 | .expect(500, 'TypeError: option domain is invalid', done) 262 | }) 263 | }) 264 | 265 | describe('"maxAge" option', function () { 266 | it('should set the "expires" attribute', function (done) { 267 | var maxAge = 86400000 268 | request(createServer(setCookieHandler('foo', 'bar', { maxAge: maxAge }))) 269 | .get('/') 270 | .expect(200) 271 | .expect(shouldSetCookieWithAttribute('foo', 'expires')) 272 | .expect(function (res) { 273 | var cookie = getCookieForName(res, 'foo') 274 | var expected = new Date(Date.parse(res.headers.date) + maxAge).toUTCString() 275 | assert.strictEqual(cookie.expires, expected) 276 | }) 277 | .end(done) 278 | }) 279 | 280 | it('should not set the "maxAge" attribute', function (done) { 281 | request(createServer(setCookieHandler('foo', 'bar', { maxAge: 86400000 }))) 282 | .get('/') 283 | .expect(200) 284 | .expect(shouldSetCookieWithAttribute('foo', 'expires')) 285 | .expect(shouldSetCookieWithoutAttribute('foo', 'maxAge')) 286 | .end(done) 287 | }) 288 | 289 | it('should not affect cookie deletion', function (done) { 290 | request(createServer(setCookieHandler('foo', null, { maxAge: 86400000 }))) 291 | .get('/') 292 | .expect(200) 293 | .expect(shouldSetCookieCount(1)) 294 | .expect(shouldSetCookieToValue('foo', '')) 295 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) 296 | .end(done) 297 | }) 298 | }) 299 | 300 | describe('"overwrite" option', function () { 301 | it('should be off by default', function (done) { 302 | request(createServer(function (req, res, cookies) { 303 | cookies.set('foo', 'bar') 304 | cookies.set('foo', 'baz') 305 | res.end() 306 | })) 307 | .get('/') 308 | .expect(200) 309 | .expect(shouldSetCookieCount(2)) 310 | .expect(shouldSetCookieToValue('foo', 'bar')) 311 | .end(done) 312 | }) 313 | 314 | it('should overwrite same cookie by name when true', function (done) { 315 | request(createServer(function (req, res, cookies) { 316 | cookies.set('foo', 'bar') 317 | cookies.set('foo', 'baz', { overwrite: true }) 318 | res.end() 319 | })) 320 | .get('/') 321 | .expect(200) 322 | .expect(shouldSetCookieCount(1)) 323 | .expect(shouldSetCookieToValue('foo', 'baz')) 324 | .end(done) 325 | }) 326 | 327 | it('should overwrite based on name only', function (done) { 328 | request(createServer(function (req, res, cookies) { 329 | cookies.set('foo', 'bar', { path: '/foo' }) 330 | cookies.set('foo', 'baz', { path: '/bar', overwrite: true }) 331 | res.end() 332 | })) 333 | .get('/') 334 | .expect(200) 335 | .expect(shouldSetCookieCount(1)) 336 | .expect(shouldSetCookieToValue('foo', 'baz')) 337 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'path', '/bar')) 338 | .end(done) 339 | }) 340 | }) 341 | 342 | describe('"partitioned" option', function () { 343 | it('should not be set by default', function (done) { 344 | request(createServer(setCookieHandler('foo', 'bar'))) 345 | .get('/') 346 | .expect(200) 347 | .expect(shouldSetCookieWithoutAttribute('foo', 'partitioned')) 348 | .end(done) 349 | }) 350 | 351 | it('should set to true', function (done) { 352 | request(createServer(setCookieHandler('foo', 'bar', { partitioned: true }))) 353 | .get('/') 354 | .expect(200) 355 | .expect(shouldSetCookieWithAttribute('foo', 'partitioned')) 356 | .end(done) 357 | }) 358 | 359 | it('should set to false', function (done) { 360 | request(createServer(setCookieHandler('foo', 'bar', { partitioned: false }))) 361 | .get('/') 362 | .expect(200) 363 | .expect(shouldSetCookieWithoutAttribute('foo', 'partitioned')) 364 | .end(done) 365 | }) 366 | }) 367 | 368 | describe('"path" option', function () { 369 | it('should default to "/"', function (done) { 370 | request(createServer(setCookieHandler('foo', 'bar'))) 371 | .get('/') 372 | .expect(200) 373 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'path', '/')) 374 | .end(done) 375 | }) 376 | 377 | it('should set to custom value', function (done) { 378 | request(createServer(setCookieHandler('foo', 'bar', { path: '/admin' }))) 379 | .get('/') 380 | .expect(200) 381 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'path', '/admin')) 382 | .end(done) 383 | }) 384 | 385 | it('should set to ""', function (done) { 386 | request(createServer(setCookieHandler('foo', 'bar', { path: '' }))) 387 | .get('/') 388 | .expect(200) 389 | .expect(shouldSetCookieWithoutAttribute('foo', 'path')) 390 | .end(done) 391 | }) 392 | 393 | it('should reject invalid value', function (done) { 394 | request(createServer(setCookieHandler('foo', 'bar', { path: 'foo\nbar' }))) 395 | .get('/') 396 | .expect(500, 'TypeError: option path is invalid', done) 397 | }) 398 | }) 399 | 400 | describe('"secure" option', function () { 401 | describe('when true', function () { 402 | it('should throw on unencrypted connection', function (done) { 403 | request(createServer(setCookieHandler('foo', 'bar', { secure: true }))) 404 | .get('/') 405 | .expect(500) 406 | .expect('Error: Cannot send secure cookie over unencrypted connection') 407 | .end(done) 408 | }) 409 | 410 | it('should set secure attribute on encrypted connection', function (done) { 411 | var server = createSecureServer(setCookieHandler('foo', 'bar', { secure: true })) 412 | 413 | request(server) 414 | .get('/') 415 | .ca(server.cert) 416 | .expect(200) 417 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 418 | .end(done) 419 | }) 420 | 421 | describe('with "secure: true" constructor option', function () { 422 | it('should set secure attribute on unencrypted connection', function (done) { 423 | var opts = { secure: true } 424 | 425 | request(createServer(opts, setCookieHandler('foo', 'bar', { secure: true }))) 426 | .get('/') 427 | .expect(200) 428 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 429 | .end(done) 430 | }) 431 | }) 432 | 433 | describe('with req.protocol === "https"', function () { 434 | it('should set secure attribute on unencrypted connection', function (done) { 435 | request(createServer(function (req, res, cookies) { 436 | req.protocol = 'https' 437 | cookies.set('foo', 'bar', { secure: true }) 438 | res.end() 439 | })) 440 | .get('/') 441 | .expect(200) 442 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 443 | .end(done) 444 | }) 445 | }) 446 | }) 447 | 448 | describe('when undefined', function () { 449 | it('should set secure attribute on encrypted connection', function (done) { 450 | var server = createSecureServer(setCookieHandler('foo', 'bar', { secure: undefined })) 451 | 452 | request(server) 453 | .get('/') 454 | .ca(server.cert) 455 | .expect(200) 456 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 457 | .end(done) 458 | }) 459 | 460 | describe('with "secure: undefined" constructor option', function () { 461 | it('should not set secure attribute on unencrypted connection', function (done) { 462 | var opts = { secure: undefined } 463 | 464 | request(createServer(opts, setCookieHandler('foo', 'bar', { secure: undefined }))) 465 | .get('/') 466 | .expect(200) 467 | .expect(shouldSetCookieWithoutAttribute('foo', 'Secure')) 468 | .end(done) 469 | }) 470 | }) 471 | 472 | describe('with req.protocol === "https"', function () { 473 | it('should set secure attribute on unencrypted connection', function (done) { 474 | request(createServer(function (req, res, cookies) { 475 | req.protocol = 'https' 476 | cookies.set('foo', 'bar', { secure: undefined }) 477 | res.end() 478 | })) 479 | .get('/') 480 | .expect(200) 481 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 482 | .end(done) 483 | }) 484 | }) 485 | }) 486 | }) 487 | 488 | describe('"secureProxy" option', function () { 489 | it('should set secure attribute over http', function (done) { 490 | request(createServer(setCookieHandler('foo', 'bar', { secureProxy: true }))) 491 | .get('/') 492 | .expect(200) 493 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 494 | .end(done) 495 | }) 496 | }) 497 | 498 | describe('"signed" option', function () { 499 | describe('when true', function () { 500 | it('should throw without .keys', function (done) { 501 | request(createServer(setCookieHandler('foo', 'bar', { signed: true }))) 502 | .get('/') 503 | .expect(500) 504 | .expect('Error: .keys required for signed cookies') 505 | .end(done) 506 | }) 507 | 508 | it('should set additional .sig cookie', function (done) { 509 | var opts = { keys: ['keyboard cat'] } 510 | 511 | request(createServer(opts, setCookieHandler('foo', 'bar', { signed: true }))) 512 | .get('/') 513 | .expect(200) 514 | .expect(shouldSetCookieCount(2)) 515 | .expect(shouldSetCookieToValue('foo', 'bar')) 516 | .expect(shouldSetCookieToValue('foo.sig', 'iW2fuCIzk9Cg_rqLT1CAqrtdWs8')) 517 | .end(done) 518 | }) 519 | 520 | it('should use first key for signature', function (done) { 521 | var opts = { keys: ['keyboard cat a', 'keyboard cat b'] } 522 | 523 | request(createServer(opts, setCookieHandler('foo', 'bar', { signed: true }))) 524 | .get('/') 525 | .expect(200) 526 | .expect(shouldSetCookieCount(2)) 527 | .expect(shouldSetCookieToValue('foo', 'bar')) 528 | .expect(shouldSetCookieToValue('foo.sig', 'tecF04p5ua6TnfYxUTDskgWSKJE')) 529 | .end(done) 530 | }) 531 | 532 | describe('when value is falsy', function () { 533 | it('should delete additional .sig cookie', function (done) { 534 | var opts = { keys: ['keyboard cat'] } 535 | request(createServer(opts, setCookieHandler('foo', null, { signed: true }))) 536 | .get('/') 537 | .expect(200) 538 | .expect(shouldSetCookieCount(2)) 539 | .expect(shouldSetCookieToValue('foo', '')) 540 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) 541 | .expect(shouldSetCookieWithAttributeAndValue('foo.sig', 'expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) 542 | .end(done) 543 | }) 544 | }) 545 | 546 | describe('with "path"', function () { 547 | it('should set additional .sig cookie with path', function (done) { 548 | var opts = { keys: ['keyboard cat'] } 549 | 550 | request(createServer(opts, setCookieHandler('foo', 'bar', { signed: true, path: '/admin' }))) 551 | .get('/') 552 | .expect(200) 553 | .expect(shouldSetCookieCount(2)) 554 | .expect(shouldSetCookieWithAttributeAndValue('foo', 'path', '/admin')) 555 | .expect(shouldSetCookieWithAttributeAndValue('foo.sig', 'path', '/admin')) 556 | .end(done) 557 | }) 558 | }) 559 | 560 | describe('with "overwrite"', function () { 561 | it('should set additional .sig cookie with httpOnly', function (done) { 562 | var opts = { keys: ['keyboard cat'] } 563 | request(createServer(opts, function (req, res, cookies) { 564 | cookies.set('foo', 'bar', { signed: true }) 565 | cookies.set('foo', 'baz', { signed: true, overwrite: true }) 566 | res.end() 567 | })) 568 | .get('/') 569 | .expect(200) 570 | .expect(shouldSetCookieCount(2)) 571 | .expect(shouldSetCookieToValue('foo', 'baz')) 572 | .expect(shouldSetCookieToValue('foo.sig', 'ptOkbbiPiGfLWRzz1yXP3XqaW4E')) 573 | .end(done) 574 | }) 575 | }) 576 | 577 | describe('with "secureProxy"', function () { 578 | it('should set additional .sig cookie with secure', function (done) { 579 | var opts = { keys: ['keyboard cat'] } 580 | 581 | request(createServer(opts, setCookieHandler('foo', 'bar', { signed: true, secureProxy: true }))) 582 | .get('/') 583 | .expect(200) 584 | .expect(shouldSetCookieCount(2)) 585 | .expect(shouldSetCookieWithAttribute('foo', 'Secure')) 586 | .expect(shouldSetCookieWithAttribute('foo.sig', 'Secure')) 587 | .end(done) 588 | }) 589 | }) 590 | }) 591 | }) 592 | }) 593 | }) 594 | 595 | describe('Cookies(req, res, [options])', function () { 596 | it('should create new cookies instance', function (done) { 597 | assertServer(done, function (req, res) { 598 | var cookies = Cookies(req, res, { keys: ['a', 'b'] }) 599 | assert.ok(cookies) 600 | assert.strictEqual(cookies.constructor, Cookies) 601 | assert.strictEqual(cookies.request, req) 602 | assert.strictEqual(cookies.response, res) 603 | assert.strictEqual(typeof cookies.keys, 'object') 604 | }) 605 | }) 606 | }) 607 | 608 | function assertServer (done, test) { 609 | var server = http.createServer(function (req, res) { 610 | try { 611 | test(req, res) 612 | res.end('OK') 613 | } catch (e) { 614 | res.statusCode = 500 615 | res.end(e.name + ': ' + e.message) 616 | } 617 | }) 618 | 619 | request(server) 620 | .get('/') 621 | .expect('OK') 622 | .expect(200) 623 | .end(done) 624 | } 625 | 626 | function createRequestListener (options, handler) { 627 | var next = handler || options 628 | var opts = next === options ? undefined : options 629 | 630 | return function (req, res) { 631 | var cookies = new Cookies(req, res, opts) 632 | 633 | try { 634 | next(req, res, cookies) 635 | } catch (e) { 636 | res.statusCode = 500 637 | res.end(e.name + ': ' + e.message) 638 | } 639 | } 640 | } 641 | 642 | function createSecureServer (options, handler) { 643 | var cert = fs.readFileSync(path.join(__dirname, 'fixtures', 'server.crt'), 'ascii') 644 | var key = fs.readFileSync(path.join(__dirname, 'fixtures', 'server.key'), 'ascii') 645 | 646 | return https.createServer({ cert: cert, key: key }) 647 | .on('request', createRequestListener(options, handler)) 648 | } 649 | 650 | function createServer (options, handler) { 651 | return http.createServer() 652 | .on('request', createRequestListener(options, handler)) 653 | } 654 | 655 | function getCookieForName (res, name) { 656 | var cookies = getCookies(res) 657 | 658 | for (var i = 0; i < cookies.length; i++) { 659 | if (cookies[i].name === name) { 660 | return cookies[i] 661 | } 662 | } 663 | } 664 | 665 | function getCookieHandler (name, options) { 666 | return function (req, res, cookies) { 667 | res.end(String(cookies.get(name, options))) 668 | } 669 | } 670 | 671 | function getCookies (res) { 672 | var setCookies = res.headers['set-cookie'] || [] 673 | return setCookies.map(parseSetCookie) 674 | } 675 | 676 | function parseSetCookie (header) { 677 | var match 678 | var pairs = [] 679 | var pattern = /\s*([^=;]+)(?:=([^;]*);?|;|$)/g 680 | 681 | while ((match = pattern.exec(header))) { 682 | pairs.push({ name: match[1], value: match[2] }) 683 | } 684 | 685 | var cookie = pairs.shift() 686 | 687 | for (var i = 0; i < pairs.length; i++) { 688 | match = pairs[i] 689 | cookie[match.name.toLowerCase()] = (match.value || true) 690 | } 691 | 692 | return cookie 693 | } 694 | 695 | function setCookieHandler (name, value, options) { 696 | return function (req, res, cookies) { 697 | cookies.set(name, value, options) 698 | res.end() 699 | } 700 | } 701 | 702 | function shouldSetCookieCount (num) { 703 | return function (res) { 704 | var count = getCookies(res).length 705 | assert.equal(count, num, 'should set cookie ' + num + ' times') 706 | } 707 | } 708 | 709 | function shouldSetCookieToValue (name, val) { 710 | return function (res) { 711 | var cookie = getCookieForName(res, name) 712 | assert.ok(cookie, 'should set cookie ' + name) 713 | assert.equal(cookie.value, val, 'should set cookie ' + name + ' to ' + val) 714 | } 715 | } 716 | 717 | function shouldSetCookieWithAttribute (name, attrib) { 718 | return function (res) { 719 | var cookie = getCookieForName(res, name) 720 | assert.ok(cookie, 'should set cookie ' + name) 721 | assert.ok((attrib.toLowerCase() in cookie), 'should set cookie with attribute ' + attrib) 722 | } 723 | } 724 | 725 | function shouldSetCookieWithAttributeAndValue (name, attrib, value) { 726 | return function (res) { 727 | var cookie = getCookieForName(res, name) 728 | assert.ok(cookie, 'should set cookie ' + name) 729 | assert.ok((attrib.toLowerCase() in cookie), 'should set cookie with attribute ' + attrib) 730 | assert.equal(cookie[attrib.toLowerCase()], value, 'should set cookie with attribute ' + attrib + ' set to ' + value) 731 | } 732 | } 733 | 734 | function shouldSetCookieWithoutAttribute (name, attrib) { 735 | return function (res) { 736 | var cookie = getCookieForName(res, name) 737 | assert.ok(cookie, 'should set cookie ' + name) 738 | assert.ok(!(attrib.toLowerCase() in cookie), 'should set cookie without attribute ' + attrib) 739 | } 740 | } 741 | --------------------------------------------------------------------------------