diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cdb36c1b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.js,*.json,*.yml}] +indent_size = 2 +indent_style = space diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..76b1021d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +.nyc_output +coverage +node_modules diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..b6b9f62f --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,15 @@ +root: true +extends: + - plugin:markdown/recommended +plugins: + - markdown +overrides: + - files: '**/*.md' + processor: 'markdown/markdown' +rules: + eol-last: error + eqeqeq: ["error", "always", { "null": "ignore" }] + indent: ["error", 2, { "MemberExpression": "off", "SwitchCase": 1 }] + no-mixed-spaces-and-tabs: error + no-trailing-spaces: error + one-var: ["error", { "initialized": "never" }] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7d2c7033 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + groups: + npm-dependencies: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..004c35ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,264 @@ +name: ci + +on: +- pull_request +- push + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + name: + - Node.js 0.8 + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + - Node.js 15.x + - Node.js 16.x + - Node.js 17.x + - Node.js 18.x + - Node.js 19.x + - Node.js 20.x + - Node.js 21.x + - Node.js 22.x + - Node.js 23.x + - Node.js 24.x + - Node.js 25.x + + include: + - name: Node.js 0.8 + node-version: "0.8" + npm-i: mocha@2.5.3 supertest@1.1.0 + npm-rm: nyc + + - name: Node.js 0.10 + node-version: "0.10" + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 0.12 + node-version: "0.12" + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 1.x + node-version: "1.8" + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 2.x + node-version: "2.5" + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: io.js 3.x + node-version: "3.3" + npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0 + + - name: Node.js 4.x + node-version: "4.9" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 5.x + node-version: "5.12" + npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + + - name: Node.js 6.x + node-version: "6.17" + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 7.x + node-version: "7.10" + npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 8.x + node-version: "8.17" + npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 9.x + node-version: "9.11" + npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 + + - name: Node.js 10.x + node-version: "10.24" + npm-i: mocha@8.4.0 supertest@6.1.6 + + - name: Node.js 11.x + node-version: "11.15" + npm-i: mocha@8.4.0 supertest@6.1.6 + + - name: Node.js 12.x + node-version: "12.22" + npm-i: mocha@9.2.2 supertest@6.1.6 + + - name: Node.js 13.x + node-version: "13.14" + npm-i: mocha@9.2.2 supertest@6.1.6 + + - name: Node.js 14.x + node-version: "14.21" + + - name: Node.js 15.x + node-version: "15.14" + npm-i: supertest@6.1.6 + + - name: Node.js 16.x + node-version: "16.20" + + - name: Node.js 17.x + node-version: "17.9" + + - name: Node.js 18.x + node-version: "18.19" + + - name: Node.js 19.x + node-version: "19.9" + + - name: Node.js 20.x + node-version: "20" + + - name: Node.js 21.x + node-version: "21" + + - name: Node.js 22.x + node-version: "22" + + - name: Node.js 23.x + node-version: "23" + + - name: Node.js 24.x + node-version: "24" + + - name: Node.js 25.x + node-version: "25" + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install Node.js ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + nvm install --alias=npm 0.10 + nvm use ${{ matrix.node-version }} + if [[ "$(npm -v)" == 1.1.* ]]; then + nvm exec npm npm install -g npm@1.1 + ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" + else + sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" + fi + npm config set strict-ssl false + fi + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: | + if [[ "$(npm config get package-lock)" == "true" ]]; then + npm config set package-lock false + else + npm config set shrinkwrap false + fi + + - name: Remove npm module(s) ${{ matrix.npm-rm }} + run: npm rm --silent --save-dev ${{ matrix.npm-rm }} + if: matrix.npm-rm != '' + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Setup Node.js version-specific dependencies + shell: bash + run: | + # eslint for linting + # - remove on Node.js < 12 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + sort -r | \ + xargs -n1 npm rm --silent --save-dev + fi + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" + + - name: Run tests + shell: bash + run: | + if npm -ps ls nyc | grep -q nyc; then + npm run test-ci + cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov" + else + npm test + fi + + - name: Lint code + if: steps.list_env.outputs.eslint != '' + run: npm run lint + + - name: Collect code coverage + if: steps.list_env.outputs.nyc != '' + run: | + if [[ -d ./coverage ]]; then + mv ./coverage "./${{ matrix.node-version }}" + mkdir ./coverage + mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}" + fi + + - name: Upload code coverage + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: steps.list_env.outputs.nyc != '' + with: + name: coverage-${{ matrix.node-version }} + path: "./coverage/${{ matrix.node-version }}" + retention-days: 1 + + coverage: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for actions/checkout to fetch code + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ./coverage + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..139ec706 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + languages: javascript + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + category: "/language:javascript" \ No newline at end of file diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..2bb15b23 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security + +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/.gitignore b/.gitignore index df9af16b..1b6fef23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.nyc_output coverage node_modules npm-debug.log +package-lock.json +/test/fixtures/server.crt +/test/fixtures/server.key diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6525236c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.6" - - "5.12" - - "6.9" - - "7.1" -sudo: false -cache: - directories: - - node_modules -before_install: - # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" - # Update Node.js modules - - "test ! -d node_modules || npm prune" - - "test ! -d node_modules || npm rebuild" -script: - # Run test script, depending on istanbul install - - "test ! -z $(npm -ps ls istanbul) || npm test" - - "test -z $(npm -ps ls istanbul) || npm run-script test-travis" -after_script: - - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" diff --git a/HISTORY.md b/HISTORY.md index f1a72f57..251d5fa7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,158 @@ unreleased ========== + * Enforce RFC 6265 section 5.1.4 path matching for the session cookie path + * Fix `secure: 'auto'` and `sameSite: 'auto'` not being resolved when `cookie` is a function + +1.19.0 / 2026-01-22 +========== + + * Add dynamic cookie options support + * Add sameSite 'auto' support for automatic SameSite attribute configuration + * deps: use tilde notation for dependencies + +1.18.2 / 2025-07-17 +========== + * deps: mocha@10.8.2 + * deps: on-headers@~1.1.0 + - Fix [CVE-2025-7339](https://www.cve.org/CVERecord?id=CVE-2025-7339) ([GHSA-76c9-3jph-rj3q](https://github.com/expressjs/on-headers/security/advisories/GHSA-76c9-3jph-rj3q)) + +1.18.1 / 2024-10-08 +========== + + * deps: cookie@0.7.2 + - Fix object assignment of `hasOwnProperty` + * deps: cookie@0.7.1 + - Allow leading dot for domain + - Although not permitted in the spec, some users expect this to work and user agents ignore the leading dot according to spec + - Add fast path for `serialize` without options, use `obj.hasOwnProperty` when parsing + * deps: cookie@0.7.0 + - perf: parse cookies ~10% faster + - fix: narrow the validation of cookies to match RFC6265 + - fix: add `main` to `package.json` for rspack + +1.18.0 / 2024-01-28 +=================== + + * Add debug log for pathname mismatch + * Add `partitioned` to `cookie` options + * Add `priority` to `cookie` options + * Fix handling errors from setting cookie + * Support any type in `secret` that `crypto.createHmac` supports + * deps: cookie@0.6.0 + - Fix `expires` option to reject invalid dates + - perf: improve default decode speed + - perf: remove slow string split in parse + * deps: cookie-signature@1.0.7 + +1.17.3 / 2022-05-11 +=================== + + * Fix resaving already-saved new session at end of request + * deps: cookie@0.4.2 + +1.17.2 / 2021-05-19 +=================== + + * Fix `res.end` patch to always commit headers + * deps: cookie@0.4.1 + * deps: safe-buffer@5.2.1 + +1.17.1 / 2020-04-16 +=================== + + * Fix internal method wrapping error on failed reloads + +1.17.0 / 2019-10-10 +=================== + + * deps: cookie@0.4.0 + - Add `SameSite=None` support + * deps: safe-buffer@5.2.0 + +1.16.2 / 2019-06-12 +=================== + + * Fix restoring `cookie.originalMaxAge` when store returns `Date` + * deps: parseurl@~1.3.3 + +1.16.1 / 2019-04-11 +=================== + + * Fix error passing `data` option to `Cookie` constructor + * Fix uncaught error from bad session data + +1.16.0 / 2019-04-10 +=================== + + * Catch invalid `cookie.maxAge` value earlier + * Deprecate setting `cookie.maxAge` to a `Date` object + * Fix issue where `resave: false` may not save altered sessions + * Remove `utils-merge` dependency + * Use `safe-buffer` for improved Buffer API + * Use `Set-Cookie` as cookie header name for compatibility + * deps: depd@~2.0.0 + - Replace internal `eval` usage with `Function` constructor + - Use instance methods on `process` to check for listeners + - perf: remove argument reassignment + * deps: on-headers@~1.0.2 + - Fix `res.writeHead` patch missing return value + +1.15.6 / 2017-09-26 +=================== + + * deps: debug@2.6.9 + * deps: parseurl@~1.3.2 + - perf: reduce overhead for full URLs + - perf: unroll the "fast-path" `RegExp` + * deps: uid-safe@~2.1.5 + - perf: remove only trailing `=` + * deps: utils-merge@1.0.1 + +1.15.5 / 2017-08-02 +=================== + + * Fix `TypeError` when `req.url` is an empty string + * deps: depd@~1.1.1 + - Remove unnecessary `Buffer` loading + +1.15.4 / 2017-07-18 +=================== + + * deps: debug@2.6.8 + +1.15.3 / 2017-05-17 +=================== + + * deps: debug@2.6.7 + - deps: ms@2.0.0 + +1.15.2 / 2017-03-26 +=================== + + * deps: debug@2.6.3 + - Fix `DEBUG_MAX_ARRAY_LENGTH` + * deps: uid-safe@~2.1.4 + - Remove `base64-url` dependency + +1.15.1 / 2017-02-10 +=================== + + * deps: debug@2.6.1 + - Fix deprecation messages in WebStorm and other editors + - Undeprecate `DEBUG_FD` set to `1` or `2` + +1.15.0 / 2017-01-22 +=================== + + * Fix detecting modified session when session contains "cookie" property * Fix resaving already-saved reloaded session at end of request - * deps: debug@2.3.2 + * deps: crc@3.4.4 + - perf: use `Buffer.from` when available + * deps: debug@2.6.0 + - Allow colors in workers + - Deprecated `DEBUG_FD` environment variable + - Use same color for same namespace - Fix error when running under React Native - deps: ms@0.7.2 * perf: remove unreachable branch in set-cookie method @@ -35,7 +185,7 @@ unreleased - Improve error message when `expires` is not a `Date` - perf: enable strict mode - perf: use for loop in parse - - perf: use string concatination for serialization + - perf: use string concatenation for serialization * deps: parseurl@~1.3.1 - perf: enable strict mode * deps: uid-safe@~2.1.1 diff --git a/README.md b/README.md index 261600f7..162ea14d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # express-session -[![NPM Version][npm-image]][npm-url] -[![NPM Downloads][downloads-image]][downloads-url] -[![Build Status][travis-image]][travis-url] +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][node-url] +[![Build Status][ci-image]][ci-url] [![Test Coverage][coveralls-image]][coveralls-url] -[![Gratipay][gratipay-image]][gratipay-url] ## Installation @@ -12,7 +11,7 @@ This is a [Node.js](https://nodejs.org/en/) module available through the [npm registry](https://www.npmjs.com/). Installation is done using the [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): -```bash +```sh $ npm install express-session ``` @@ -50,6 +49,26 @@ For a list of stores, see [compatible session stores](#compatible-session-stores Settings object for the session ID cookie. The default value is `{ path: '/', httpOnly: true, secure: false, maxAge: null }`. +In addition to providing a static object, you can also pass a callback function to dynamically generate the cookie options for each request. The callback receives the `req` object as its argument and should return an object containing the cookie settings. + +```js +var app = express() +app.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true, + cookie: function(req) { + var match = req.url.match(/^\/([^/]+)/); + return { + path: match ? '/' + match[1] : '/', + httpOnly: true, + secure: req.secure || false, + maxAge: 60000 + } + } +})) +``` + The following are options that can be set in this object. ##### cookie.domain @@ -90,26 +109,73 @@ no maximum age is set. **Note** If both `expires` and `maxAge` are set in the options, then the last one defined in the object is what is used. +##### cookie.partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. +By default, the `Partitioned` attribute is not set. + +**Note** This is an attribute that has not yet been fully standardized, and may +change in the future. This also means many clients may ignore this attribute until +they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + ##### cookie.path Specifies the value for the `Path` `Set-Cookie`. By default, this is set to `'/'`, which is the root path of the domain. +Since 1.19.1, path matching follows [RFC 6265 section 5.1.4][rfc-6265-5.1.4]. This means +the session middleware will only activate when the request path is an exact match or falls +under a segment boundary of the cookie path. For example, a cookie path of `/admin` will +match `/admin` and `/admin/users` but will **not** match `/administrator`. Prior versions +used a simple prefix check that did not enforce segment boundaries. + +##### cookie.priority + +Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + + - `'low'` will set the `Priority` attribute to `Low`. + - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + - `'high'` will set the `Priority` attribute to `High`. + +More information about the different priority levels can be found in +[the specification][rfc-west-cookie-priority-00-4.1]. + +**Note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + ##### cookie.sameSite Specifies the `boolean` or `string` to be the value for the `SameSite` `Set-Cookie` attribute. +By default, this is `false`. - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. - `false` will not set the `SameSite` attribute. - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. + - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `'auto'` will set the `SameSite` attribute to `None` for secure connections and `Lax` for non-secure connections. -More information about the different enforcement levels can be found in the specification -https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1.1 +More information about the different enforcement levels can be found in +[the specification][rfc-6265bis-03-4.1.2.7]. **Note** This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it. +**Note** There is a [draft spec](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01) +that requires that the `Secure` attribute be set to `true` when the `SameSite` attribute has been +set to `'none'`. Some web browsers or other clients may be adopting this specification. + +The `cookie.sameSite` option can also be set to the special value `'auto'` to have +this setting automatically match the determined security of the connection. When the connection +is secure (HTTPS), the `SameSite` attribute will be set to `None` to enable cross-site usage. +When the connection is not secure (HTTP), the `SameSite` attribute will be set to `Lax` for +better security while maintaining functionality. This is useful when the Express `"trust proxy"` +setting is properly setup to simplify development vs production configuration, particularly +for SAML authentication scenarios. + ##### cookie.secure Specifies the `boolean` value for the `Secure` `Set-Cookie` attribute. When truthy, @@ -228,15 +294,24 @@ likely need `resave: true`. ##### rolling -Force a session identifier cookie to be set on every response. The expiration +Force the session identifier cookie to be set on every response. The expiration is reset to the original [`maxAge`](#cookiemaxage), resetting the expiration countdown. The default value is `false`. +With this enabled, the session identifier cookie will expire in +[`maxAge`](#cookiemaxage) since the last response was sent instead of in +[`maxAge`](#cookiemaxage) since the session was last modified by the server. + +This is typically used in conjunction with short, non-session-length +[`maxAge`](#cookiemaxage) values to provide a quick timeout of the session data +with reduced potential of it occurring during on going server interactions. + **Note** When this option is set to `true` but the `saveUninitialized` option is set to `false`, the cookie will not be set on a response with an uninitialized -session. +session. This option only modifies the behavior when an existing session was +loaded for the request. ##### saveUninitialized @@ -260,10 +335,29 @@ it to be saved. *This has been fixed in PassportJS 0.3.0* **Required option** -This is the secret used to sign the session ID cookie. This can be either a string -for a single secret, or an array of multiple secrets. If an array of secrets is -provided, only the first element will be used to sign the session ID cookie, while -all the elements will be considered when verifying the signature in requests. +This is the secret used to sign the session ID cookie. The secret can be any type +of value that is supported by Node.js `crypto.createHmac` (like a string or a +`Buffer`). This can be either a single secret, or an array of multiple secrets. If +an array of secrets is provided, only the first element will be used to sign the +session ID cookie, while all the elements will be considered when verifying the +signature in requests. The secret itself should be not easily parsed by a human and +would best be a random set of characters. A best practice may include: + + - The use of environment variables to store the secret, ensuring the secret itself + does not exist in your repository. + - Periodic updates of the secret, while ensuring the previous secret is in the + array. + +Using a secret that cannot be guessed will reduce the ability to hijack a session to +only guessing the session ID (as determined by the `genid` option). + +Changing the secret value will invalidate all existing sessions. In order to rotate +the secret without invalidating sessions, provide an array of secrets, with the new +secret as first element of the array, and including previous secrets as the later +elements. + +**Note** HMAC-256 is used to sign the session ID. For this reason, the secret should +contain at least 32 bytes of entropy. ##### store @@ -292,15 +386,14 @@ app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) // Access the session as req.session app.get('/', function(req, res, next) { - var sess = req.session - if (sess.views) { - sess.views++ + if (req.session.views) { + req.session.views++ res.setHeader('Content-Type', 'text/html') - res.write('

views: ' + sess.views + '

') - res.write('

expires in: ' + (sess.cookie.maxAge / 1000) + 's

') + res.write('

views: ' + req.session.views + '

') + res.write('

expires in: ' + (req.session.cookie.maxAge / 1000) + 's

') res.end() } else { - sess.views = 1 + req.session.views = 1 res.end('welcome to the session demo. refresh!') } }) @@ -351,8 +444,8 @@ session data has been altered (though this behavior can be altered with various options in the middleware constructor). Because of this, typically this method does not need to be called. -There are some cases where it is useful to call this method, for example, long- -lived requests or in WebSockets. +There are some cases where it is useful to call this method, for example, +redirects, long-lived requests or in WebSockets. ```js req.session.save(function(err) { @@ -367,8 +460,10 @@ not necessary to call, as the session middleware does this for you. ### req.session.id -Each session has a unique ID associated with it. This property will -contain the session ID and cannot be modified. +Each session has a unique ID associated with it. This property is an +alias of [`req.sessionID`](#reqsessionid-1) and cannot be modified. +It has been added to make the session ID accessible from the `session` +object. ### req.session.cookie @@ -392,13 +487,18 @@ req.session.cookie.maxAge = hour For example when `maxAge` is set to `60000` (one minute), and 30 seconds has elapsed it will return `30000` until the current request has completed, -at which time `req.session.touch()` is called to reset `req.session.maxAge` -to its original value. +at which time `req.session.touch()` is called to reset +`req.session.cookie.maxAge` to its original value. ```js req.session.cookie.maxAge // => 30000 ``` +#### Cookie.originalMaxAge + +The `req.session.cookie.originalMaxAge` property returns the original +`maxAge` (time-to-live), in milliseconds, of the session cookie. + ### req.sessionID To get the ID of the loaded session, access the request property @@ -487,184 +587,321 @@ module. Please make a PR to add additional modules :) [![★][aerospike-session-store-image] aerospike-session-store][aerospike-session-store-url] A session store using [Aerospike](http://www.aerospike.com/). [aerospike-session-store-url]: https://www.npmjs.com/package/aerospike-session-store -[aerospike-session-store-image]: https://img.shields.io/github/stars/aerospike/aerospike-session-store-expressjs.svg?label=%E2%98%85 +[aerospike-session-store-image]: https://badgen.net/github/stars/aerospike/aerospike-session-store-expressjs?label=%E2%98%85 + +[![★][better-sqlite3-session-store-image] better-sqlite3-session-store][better-sqlite3-session-store-url] A session store based on [better-sqlite3](https://github.com/JoshuaWise/better-sqlite3). + +[better-sqlite3-session-store-url]: https://www.npmjs.com/package/better-sqlite3-session-store +[better-sqlite3-session-store-image]: https://badgen.net/github/stars/timdaub/better-sqlite3-session-store?label=%E2%98%85 [![★][cassandra-store-image] cassandra-store][cassandra-store-url] An Apache Cassandra-based session store. [cassandra-store-url]: https://www.npmjs.com/package/cassandra-store -[cassandra-store-image]: https://img.shields.io/github/stars/webcc/cassandra-store.svg?label=%E2%98%85 +[cassandra-store-image]: https://badgen.net/github/stars/webcc/cassandra-store?label=%E2%98%85 [![★][cluster-store-image] cluster-store][cluster-store-url] A wrapper for using in-process / embedded stores - such as SQLite (via knex), leveldb, files, or memory - with node cluster (desirable for Raspberry Pi 2 and other multi-core embedded devices). [cluster-store-url]: https://www.npmjs.com/package/cluster-store -[cluster-store-image]: https://img.shields.io/github/stars/coolaj86/cluster-store.svg?label=%E2%98%85 +[cluster-store-image]: https://badgen.net/github/stars/coolaj86/cluster-store?label=%E2%98%85 + +[![★][connect-arango-image] connect-arango][connect-arango-url] An ArangoDB-based session store. + +[connect-arango-url]: https://www.npmjs.com/package/connect-arango +[connect-arango-image]: https://badgen.net/github/stars/AlexanderArvidsson/connect-arango?label=%E2%98%85 [![★][connect-azuretables-image] connect-azuretables][connect-azuretables-url] An [Azure Table Storage](https://azure.microsoft.com/en-gb/services/storage/tables/)-based session store. [connect-azuretables-url]: https://www.npmjs.com/package/connect-azuretables -[connect-azuretables-image]: https://img.shields.io/github/stars/mike-goodwin/connect-azuretables.svg?label=%E2%98%85 +[connect-azuretables-image]: https://badgen.net/github/stars/mike-goodwin/connect-azuretables?label=%E2%98%85 + +[![★][connect-cloudant-store-image] connect-cloudant-store][connect-cloudant-store-url] An [IBM Cloudant](https://cloudant.com/)-based session store. + +[connect-cloudant-store-url]: https://www.npmjs.com/package/connect-cloudant-store +[connect-cloudant-store-image]: https://badgen.net/github/stars/adriantanasa/connect-cloudant-store?label=%E2%98%85 + +[![★][connect-cosmosdb-image] connect-cosmosdb][connect-cosmosdb-url] An Azure [Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db/)-based session store. + +[connect-cosmosdb-url]: https://www.npmjs.com/package/connect-cosmosdb +[connect-cosmosdb-image]: https://badgen.net/github/stars/thekillingspree/connect-cosmosdb?label=%E2%98%85 [![★][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store. [connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase -[connect-couchbase-image]: https://img.shields.io/github/stars/christophermina/connect-couchbase.svg?label=%E2%98%85 +[connect-couchbase-image]: https://badgen.net/github/stars/christophermina/connect-couchbase?label=%E2%98%85 [![★][connect-datacache-image] connect-datacache][connect-datacache-url] An [IBM Bluemix Data Cache](http://www.ibm.com/cloud-computing/bluemix/)-based session store. [connect-datacache-url]: https://www.npmjs.com/package/connect-datacache -[connect-datacache-image]: https://img.shields.io/github/stars/adriantanasa/connect-datacache.svg?label=%E2%98%85 +[connect-datacache-image]: https://badgen.net/github/stars/adriantanasa/connect-datacache?label=%E2%98%85 + +[![★][@google-cloud/connect-datastore-image] @google-cloud/connect-datastore][@google-cloud/connect-datastore-url] A [Google Cloud Datastore](https://cloud.google.com/datastore/docs/concepts/overview)-based session store. + +[@google-cloud/connect-datastore-url]: https://www.npmjs.com/package/@google-cloud/connect-datastore +[@google-cloud/connect-datastore-image]: https://badgen.net/github/stars/GoogleCloudPlatform/cloud-datastore-session-node?label=%E2%98%85 [![★][connect-db2-image] connect-db2][connect-db2-url] An IBM DB2-based session store built using [ibm_db](https://www.npmjs.com/package/ibm_db) module. [connect-db2-url]: https://www.npmjs.com/package/connect-db2 -[connect-db2-image]: https://img.shields.io/github/stars/wallali/connect-db2.svg?label=%E2%98%85 +[connect-db2-image]: https://badgen.net/github/stars/wallali/connect-db2?label=%E2%98%85 [![★][connect-dynamodb-image] connect-dynamodb][connect-dynamodb-url] A DynamoDB-based session store. -[connect-dynamodb-url]: https://github.com/ca98am79/connect-dynamodb -[connect-dynamodb-image]: https://img.shields.io/github/stars/ca98am79/connect-dynamodb.svg?label=%E2%98%85 +[connect-dynamodb-url]: https://www.npmjs.com/package/connect-dynamodb +[connect-dynamodb-image]: https://badgen.net/github/stars/ca98am79/connect-dynamodb?label=%E2%98%85 + +[![★][@google-cloud/connect-firestore-image] @google-cloud/connect-firestore][@google-cloud/connect-firestore-url] A [Google Cloud Firestore](https://cloud.google.com/firestore/docs/overview)-based session store. + +[@google-cloud/connect-firestore-url]: https://www.npmjs.com/package/@google-cloud/connect-firestore +[@google-cloud/connect-firestore-image]: https://badgen.net/github/stars/googleapis/nodejs-firestore-session?label=%E2%98%85 + +[![★][connect-hazelcast-image] connect-hazelcast][connect-hazelcast-url] Hazelcast session store for Connect and Express. + +[connect-hazelcast-url]: https://www.npmjs.com/package/connect-hazelcast +[connect-hazelcast-image]: https://badgen.net/github/stars/huseyinbabal/connect-hazelcast?label=%E2%98%85 [![★][connect-loki-image] connect-loki][connect-loki-url] A Loki.js-based session store. [connect-loki-url]: https://www.npmjs.com/package/connect-loki -[connect-loki-image]: https://img.shields.io/github/stars/Requarks/connect-loki.svg?label=%E2%98%85 +[connect-loki-image]: https://badgen.net/github/stars/Requarks/connect-loki?label=%E2%98%85 -[![★][connect-ml-image] connect-ml][connect-ml-url] A MarkLogic Server-based session store. +[![★][connect-lowdb-image] connect-lowdb][connect-lowdb-url] A lowdb-based session store. -[connect-ml-url]: https://www.npmjs.com/package/connect-ml -[connect-ml-image]: https://img.shields.io/github/stars/bluetorch/connect-ml.svg?label=%E2%98%85 +[connect-lowdb-url]: https://www.npmjs.com/package/connect-lowdb +[connect-lowdb-image]: https://badgen.net/github/stars/travishorn/connect-lowdb?label=%E2%98%85 + +[![★][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store. + +[connect-memcached-url]: https://www.npmjs.com/package/connect-memcached +[connect-memcached-image]: https://badgen.net/github/stars/balor/connect-memcached?label=%E2%98%85 -[![★][connect-mssql-image] connect-mssql][connect-mssql-url] A SQL Server-based session store. +[![★][connect-memjs-image] connect-memjs][connect-memjs-url] A memcached-based session store using +[memjs](https://www.npmjs.com/package/memjs) as the memcached client. -[connect-mssql-url]: https://www.npmjs.com/package/connect-mssql -[connect-mssql-image]: https://img.shields.io/github/stars/patriksimek/connect-mssql.svg?label=%E2%98%85 +[connect-memjs-url]: https://www.npmjs.com/package/connect-memjs +[connect-memjs-image]: https://badgen.net/github/stars/liamdon/connect-memjs?label=%E2%98%85 + +[![★][connect-ml-image] connect-ml][connect-ml-url] A MarkLogic Server-based session store. + +[connect-ml-url]: https://www.npmjs.com/package/connect-ml +[connect-ml-image]: https://badgen.net/github/stars/bluetorch/connect-ml?label=%E2%98%85 [![★][connect-monetdb-image] connect-monetdb][connect-monetdb-url] A MonetDB-based session store. [connect-monetdb-url]: https://www.npmjs.com/package/connect-monetdb -[connect-monetdb-image]: https://img.shields.io/github/stars/MonetDB/npm-connect-monetdb.svg?label=%E2%98%85 +[connect-monetdb-image]: https://badgen.net/github/stars/MonetDB/npm-connect-monetdb?label=%E2%98%85 [![★][connect-mongo-image] connect-mongo][connect-mongo-url] A MongoDB-based session store. [connect-mongo-url]: https://www.npmjs.com/package/connect-mongo -[connect-mongo-image]: https://img.shields.io/github/stars/kcbanner/connect-mongo.svg?label=%E2%98%85 +[connect-mongo-image]: https://badgen.net/github/stars/kcbanner/connect-mongo?label=%E2%98%85 [![★][connect-mongodb-session-image] connect-mongodb-session][connect-mongodb-session-url] Lightweight MongoDB-based session store built and maintained by MongoDB. [connect-mongodb-session-url]: https://www.npmjs.com/package/connect-mongodb-session -[connect-mongodb-session-image]: https://img.shields.io/github/stars/mongodb-js/connect-mongodb-session.svg?label=%E2%98%85 +[connect-mongodb-session-image]: https://badgen.net/github/stars/mongodb-js/connect-mongodb-session?label=%E2%98%85 + +[![★][connect-mssql-v2-image] connect-mssql-v2][connect-mssql-v2-url] A Microsoft SQL Server-based session store based on [connect-mssql](https://www.npmjs.com/package/connect-mssql). + +[connect-mssql-v2-url]: https://www.npmjs.com/package/connect-mssql-v2 +[connect-mssql-v2-image]: https://badgen.net/github/stars/jluboff/connect-mssql-v2?label=%E2%98%85 + +[![★][connect-neo4j-image] connect-neo4j][connect-neo4j-url] A [Neo4j](https://neo4j.com)-based session store. + +[connect-neo4j-url]: https://www.npmjs.com/package/connect-neo4j +[connect-neo4j-image]: https://badgen.net/github/stars/MaxAndersson/connect-neo4j?label=%E2%98%85 + +[![★][connect-ottoman-image] connect-ottoman][connect-ottoman-url] A [couchbase ottoman](http://www.couchbase.com/)-based session store. + +[connect-ottoman-url]: https://www.npmjs.com/package/connect-ottoman +[connect-ottoman-image]: https://badgen.net/github/stars/noiissyboy/connect-ottoman?label=%E2%98%85 [![★][connect-pg-simple-image] connect-pg-simple][connect-pg-simple-url] A PostgreSQL-based session store. [connect-pg-simple-url]: https://www.npmjs.com/package/connect-pg-simple -[connect-pg-simple-image]: https://img.shields.io/github/stars/voxpelli/node-connect-pg-simple.svg?label=%E2%98%85 +[connect-pg-simple-image]: https://badgen.net/github/stars/voxpelli/node-connect-pg-simple?label=%E2%98%85 [![★][connect-redis-image] connect-redis][connect-redis-url] A Redis-based session store. [connect-redis-url]: https://www.npmjs.com/package/connect-redis -[connect-redis-image]: https://img.shields.io/github/stars/tj/connect-redis.svg?label=%E2%98%85 +[connect-redis-image]: https://badgen.net/github/stars/tj/connect-redis?label=%E2%98%85 -[![★][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store. +[![★][connect-session-firebase-image] connect-session-firebase][connect-session-firebase-url] A session store based on the [Firebase Realtime Database](https://firebase.google.com/docs/database/) -[connect-memcached-url]: https://www.npmjs.com/package/connect-memcached -[connect-memcached-image]: https://img.shields.io/github/stars/balor/connect-memcached.svg?label=%E2%98%85 +[connect-session-firebase-url]: https://www.npmjs.com/package/connect-session-firebase +[connect-session-firebase-image]: https://badgen.net/github/stars/benweier/connect-session-firebase?label=%E2%98%85 [![★][connect-session-knex-image] connect-session-knex][connect-session-knex-url] A session store using [Knex.js](http://knexjs.org/), which is a SQL query builder for PostgreSQL, MySQL, MariaDB, SQLite3, and Oracle. [connect-session-knex-url]: https://www.npmjs.com/package/connect-session-knex -[connect-session-knex-image]: https://img.shields.io/github/stars/llambda/connect-session-knex.svg?label=%E2%98%85 +[connect-session-knex-image]: https://badgen.net/github/stars/llambda/connect-session-knex?label=%E2%98%85 [![★][connect-session-sequelize-image] connect-session-sequelize][connect-session-sequelize-url] A session store using -[Sequelize.js](http://sequelizejs.com/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. +[Sequelize.js](https://sequelize.org/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. [connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize -[connect-session-sequelize-image]: https://img.shields.io/github/stars/mweibel/connect-session-sequelize.svg?label=%E2%98%85 +[connect-session-sequelize-image]: https://badgen.net/github/stars/mweibel/connect-session-sequelize?label=%E2%98%85 + +[![★][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store. + +[connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3 +[connect-sqlite3-image]: https://badgen.net/github/stars/rawberg/connect-sqlite3?label=%E2%98%85 + +[![★][connect-typeorm-image] connect-typeorm][connect-typeorm-url] A [TypeORM](https://github.com/typeorm/typeorm)-based session store. + +[connect-typeorm-url]: https://www.npmjs.com/package/connect-typeorm +[connect-typeorm-image]: https://badgen.net/github/stars/makepost/connect-typeorm?label=%E2%98%85 + +[![★][couchdb-expression-image] couchdb-expression][couchdb-expression-url] A [CouchDB](https://couchdb.apache.org/)-based session store. + +[couchdb-expression-url]: https://www.npmjs.com/package/couchdb-expression +[couchdb-expression-image]: https://badgen.net/github/stars/tkshnwesper/couchdb-expression?label=%E2%98%85 + +[![★][dynamodb-store-image] dynamodb-store][dynamodb-store-url] A DynamoDB-based session store. + +[dynamodb-store-url]: https://www.npmjs.com/package/dynamodb-store +[dynamodb-store-image]: https://badgen.net/github/stars/rafaelrpinto/dynamodb-store?label=%E2%98%85 + +[![★][express-etcd-image] express-etcd][express-etcd-url] An [etcd](https://github.com/stianeikeland/node-etcd) based session store. + +[express-etcd-url]: https://www.npmjs.com/package/express-etcd +[express-etcd-image]: https://badgen.net/github/stars/gildean/express-etcd?label=%E2%98%85 [![★][express-mysql-session-image] express-mysql-session][express-mysql-session-url] A session store using native [MySQL](https://www.mysql.com/) via the [node-mysql](https://github.com/felixge/node-mysql) module. [express-mysql-session-url]: https://www.npmjs.com/package/express-mysql-session -[express-mysql-session-image]: https://img.shields.io/github/stars/chill117/express-mysql-session.svg?label=%E2%98%85 +[express-mysql-session-image]: https://badgen.net/github/stars/chill117/express-mysql-session?label=%E2%98%85 -[![★][express-sessions-image] express-sessions][express-sessions-url]: A session store supporting both MongoDB and Redis. -[express-sessions-url]: https://www.npmjs.com/package/express-sessions -[express-sessions-image]: https://img.shields.io/github/stars/konteck/express-sessions.svg?label=%E2%98%85 +[![★][express-nedb-session-image] express-nedb-session][express-nedb-session-url] A NeDB-based session store. -[![★][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store. +[express-nedb-session-url]: https://www.npmjs.com/package/express-nedb-session +[express-nedb-session-image]: https://badgen.net/github/stars/louischatriot/express-nedb-session?label=%E2%98%85 -[connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3 -[connect-sqlite3-image]: https://img.shields.io/github/stars/rawberg/connect-sqlite3.svg?label=%E2%98%85 +[![★][express-oracle-session-image] express-oracle-session][express-oracle-session-url] A session store using native +[oracle](https://www.oracle.com/) via the [node-oracledb](https://www.npmjs.com/package/oracledb) module. -[![★][documentdb-session-image] documentdb-session][documentdb-session-url] A session store for Microsoft Azure's [DocumentDB](https://azure.microsoft.com/en-us/services/documentdb/) NoSQL database service. +[express-oracle-session-url]: https://www.npmjs.com/package/express-oracle-session +[express-oracle-session-image]: https://badgen.net/github/stars/slumber86/express-oracle-session?label=%E2%98%85 -[documentdb-session-url]: https://www.npmjs.com/package/documentdb-session -[documentdb-session-image]: https://img.shields.io/github/stars/dwhieb/documentdb-session.svg?label=%E2%98%85 +[![★][express-session-cache-manager-image] express-session-cache-manager][express-session-cache-manager-url] +A store that implements [cache-manager](https://www.npmjs.com/package/cache-manager), which supports +a [variety of storage types](https://www.npmjs.com/package/cache-manager#store-engines). -[![★][express-nedb-session-image] express-nedb-session][express-nedb-session-url] A NeDB-based session store. +[express-session-cache-manager-url]: https://www.npmjs.com/package/express-session-cache-manager +[express-session-cache-manager-image]: https://badgen.net/github/stars/theogravity/express-session-cache-manager?label=%E2%98%85 -[express-nedb-session-url]: https://www.npmjs.com/package/express-nedb-session -[express-nedb-session-image]: https://img.shields.io/github/stars/louischatriot/express-nedb-session.svg?label=%E2%98%85 +[![★][express-session-etcd3-image] express-session-etcd3][express-session-etcd3-url] An [etcd3](https://github.com/mixer/etcd3) based session store. + +[express-session-etcd3-url]: https://www.npmjs.com/package/express-session-etcd3 +[express-session-etcd3-image]: https://badgen.net/github/stars/willgm/express-session-etcd3?label=%E2%98%85 [![★][express-session-level-image] express-session-level][express-session-level-url] A [LevelDB](https://github.com/Level/levelup) based session store. [express-session-level-url]: https://www.npmjs.com/package/express-session-level -[express-session-level-image]: https://img.shields.io/github/stars/tgohn/express-session-level.svg?label=%E2%98%85 +[express-session-level-image]: https://badgen.net/github/stars/tgohn/express-session-level?label=%E2%98%85 -[![★][express-etcd-image] express-etcd][express-etcd-url] An [etcd](https://github.com/stianeikeland/node-etcd) based session store. +[![★][express-session-rsdb-image] express-session-rsdb][express-session-rsdb-url] Session store based on Rocket-Store: A very simple, super fast and yet powerful, flat file database. -[express-etcd-url]: https://www.npmjs.com/package/express-etcd -[express-etcd-image]: https://img.shields.io/github/stars/gildean/express-etcd.svg?label=%E2%98%85 +[express-session-rsdb-url]: https://www.npmjs.com/package/express-session-rsdb +[express-session-rsdb-image]: https://badgen.net/github/stars/paragi/express-session-rsdb?label=%E2%98%85 + +[![★][express-sessions-image] express-sessions][express-sessions-url] A session store supporting both MongoDB and Redis. + +[express-sessions-url]: https://www.npmjs.com/package/express-sessions +[express-sessions-image]: https://badgen.net/github/stars/konteck/express-sessions?label=%E2%98%85 + +[![★][firestore-store-image] firestore-store][firestore-store-url] A [Firestore](https://github.com/hendrysadrak/firestore-store)-based session store. + +[firestore-store-url]: https://www.npmjs.com/package/firestore-store +[firestore-store-image]: https://badgen.net/github/stars/hendrysadrak/firestore-store?label=%E2%98%85 [![★][fortune-session-image] fortune-session][fortune-session-url] A [Fortune.js](https://github.com/fortunejs/fortune) based session store. Supports all backends supported by Fortune (MongoDB, Redis, Postgres, NeDB). [fortune-session-url]: https://www.npmjs.com/package/fortune-session -[fortune-session-image]: https://img.shields.io/github/stars/aliceklipper/fortune-session.svg?label=%E2%98%85 +[fortune-session-image]: https://badgen.net/github/stars/aliceklipper/fortune-session?label=%E2%98%85 [![★][hazelcast-store-image] hazelcast-store][hazelcast-store-url] A Hazelcast-based session store built on the [Hazelcast Node Client](https://www.npmjs.com/package/hazelcast-client). [hazelcast-store-url]: https://www.npmjs.com/package/hazelcast-store -[hazelcast-store-image]: https://img.shields.io/github/stars/jackspaniel/hazelcast-store.svg?label=%E2%98%85 +[hazelcast-store-image]: https://badgen.net/github/stars/jackspaniel/hazelcast-store?label=%E2%98%85 [![★][level-session-store-image] level-session-store][level-session-store-url] A LevelDB-based session store. [level-session-store-url]: https://www.npmjs.com/package/level-session-store -[level-session-store-image]: https://img.shields.io/github/stars/scriptollc/level-session-store.svg?label=%E2%98%85 +[level-session-store-image]: https://badgen.net/github/stars/toddself/level-session-store?label=%E2%98%85 + +[![★][lowdb-session-store-image] lowdb-session-store][lowdb-session-store-url] A [lowdb](https://www.npmjs.com/package/lowdb)-based session store. + +[lowdb-session-store-url]: https://www.npmjs.com/package/lowdb-session-store +[lowdb-session-store-image]: https://badgen.net/github/stars/fhellwig/lowdb-session-store?label=%E2%98%85 [![★][medea-session-store-image] medea-session-store][medea-session-store-url] A Medea-based session store. [medea-session-store-url]: https://www.npmjs.com/package/medea-session-store -[medea-session-store-image]: https://img.shields.io/github/stars/BenjaminVadant/medea-session-store.svg?label=%E2%98%85 +[medea-session-store-image]: https://badgen.net/github/stars/BenjaminVadant/medea-session-store?label=%E2%98%85 + +[![★][memorystore-image] memorystore][memorystore-url] A memory session store made for production. + +[memorystore-url]: https://www.npmjs.com/package/memorystore +[memorystore-image]: https://badgen.net/github/stars/roccomuso/memorystore?label=%E2%98%85 [![★][mssql-session-store-image] mssql-session-store][mssql-session-store-url] A SQL Server-based session store. [mssql-session-store-url]: https://www.npmjs.com/package/mssql-session-store -[mssql-session-store-image]: https://img.shields.io/github/stars/jwathen/mssql-session-store.svg?label=%E2%98%85 +[mssql-session-store-image]: https://badgen.net/github/stars/jwathen/mssql-session-store?label=%E2%98%85 [![★][nedb-session-store-image] nedb-session-store][nedb-session-store-url] An alternate NeDB-based (either in-memory or file-persisted) session store. [nedb-session-store-url]: https://www.npmjs.com/package/nedb-session-store -[nedb-session-store-image]: https://img.shields.io/github/stars/JamesMGreene/nedb-session-store.svg?label=%E2%98%85 +[nedb-session-store-image]: https://badgen.net/github/stars/JamesMGreene/nedb-session-store?label=%E2%98%85 + +[![★][@quixo3/prisma-session-store-image] @quixo3/prisma-session-store][@quixo3/prisma-session-store-url] A session store for the [Prisma Framework](https://www.prisma.io). + +[@quixo3/prisma-session-store-url]: https://www.npmjs.com/package/@quixo3/prisma-session-store +[@quixo3/prisma-session-store-image]: https://badgen.net/github/stars/kleydon/prisma-session-store?label=%E2%98%85 + +[![★][restsession-image] restsession][restsession-url] Store sessions utilizing a RESTful API + +[restsession-url]: https://www.npmjs.com/package/restsession +[restsession-image]: https://badgen.net/github/stars/jankal/restsession?label=%E2%98%85 -[![★][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](http://sequelizejs.com/). +[![★][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](https://sequelize.org/). [sequelstore-connect-url]: https://www.npmjs.com/package/sequelstore-connect -[sequelstore-connect-image]: https://img.shields.io/github/stars/MattMcFarland/sequelstore-connect.svg?label=%E2%98%85 +[sequelstore-connect-image]: https://badgen.net/github/stars/MattMcFarland/sequelstore-connect?label=%E2%98%85 [![★][session-file-store-image] session-file-store][session-file-store-url] A file system-based session store. [session-file-store-url]: https://www.npmjs.com/package/session-file-store -[session-file-store-image]: https://img.shields.io/github/stars/valery-barysok/session-file-store.svg?label=%E2%98%85 +[session-file-store-image]: https://badgen.net/github/stars/valery-barysok/session-file-store?label=%E2%98%85 -[![★][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store. +[![★][session-pouchdb-store-image] session-pouchdb-store][session-pouchdb-store-url] Session store for PouchDB / CouchDB. Accepts embedded, custom, or remote PouchDB instance and realtime synchronization. -[session-rethinkdb-url]: https://www.npmjs.com/package/session-rethinkdb -[session-rethinkdb-image]: https://img.shields.io/github/stars/llambda/session-rethinkdb.svg?label=%E2%98%85 +[session-pouchdb-store-url]: https://www.npmjs.com/package/session-pouchdb-store +[session-pouchdb-store-image]: https://badgen.net/github/stars/solzimer/session-pouchdb-store?label=%E2%98%85 -## Example +[![★][@databunker/session-store-image] @databunker/session-store][@databunker/session-store-url] A [Databunker](https://databunker.org/)-based encrypted session store. + +[@databunker/session-store-url]: https://www.npmjs.com/package/@databunker/session-store +[@databunker/session-store-image]: https://badgen.net/github/stars/securitybunker/databunker-session-store?label=%E2%98%85 + +[![★][sessionstore-image] sessionstore][sessionstore-url] A session store that works with various databases. + +[sessionstore-url]: https://www.npmjs.com/package/sessionstore +[sessionstore-image]: https://badgen.net/github/stars/adrai/sessionstore?label=%E2%98%85 + +[![★][tch-nedb-session-image] tch-nedb-session][tch-nedb-session-url] A file system session store based on NeDB. + +[tch-nedb-session-url]: https://www.npmjs.com/package/tch-nedb-session +[tch-nedb-session-image]: https://badgen.net/github/stars/tomaschyly/NeDBSession?label=%E2%98%85 + +## Examples + +### View counter A simple example using `express-session` to store page views for a user. @@ -682,17 +919,15 @@ app.use(session({ })) app.use(function (req, res, next) { - var views = req.session.views - - if (!views) { - views = req.session.views = {} + if (!req.session.views) { + req.session.views = {} } // get the url pathname var pathname = parseurl(req).pathname // count the views - views[pathname] = (views[pathname] || 0) + 1 + req.session.views[pathname] = (req.session.views[pathname] || 0) + 1 next() }) @@ -704,19 +939,120 @@ app.get('/foo', function (req, res, next) { app.get('/bar', function (req, res, next) { res.send('you viewed this page ' + req.session.views['/bar'] + ' times') }) + +app.listen(3000) +``` + +### User login + +A simple example using `express-session` to keep a user log in session. + +```js +var escapeHtml = require('escape-html') +var express = require('express') +var session = require('express-session') + +var app = express() + +app.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true +})) + +// middleware to test if authenticated +function isAuthenticated (req, res, next) { + if (req.session.user) next() + else next('route') +} + +app.get('/', isAuthenticated, function (req, res) { + // this is only called when there is an authentication user due to isAuthenticated + res.send('hello, ' + escapeHtml(req.session.user) + '!' + + ' Logout') +}) + +app.get('/', function (req, res) { + res.send('
' + + 'Username:
' + + 'Password:
' + + '
') +}) + +app.post('/login', express.urlencoded({ extended: false }), function (req, res) { + // login logic to validate req.body.user and req.body.pass + // would be implemented here. for this example any combo works + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function (err) { + if (err) next(err) + + // store user information in session, typically a user id + req.session.user = req.body.user + + // save the session before redirection to ensure page + // load does not happen before session is saved + req.session.save(function (err) { + if (err) return next(err) + res.redirect('/') + }) + }) +}) + +app.get('/logout', function (req, res, next) { + // logout logic + + // clear the user from the session object and save. + // this will ensure that re-using the old session id + // does not have a logged in user + req.session.user = null + req.session.save(function (err) { + if (err) next(err) + + // regenerate the session, which is good practice to help + // guard against forms of session fixation + req.session.regenerate(function (err) { + if (err) next(err) + res.redirect('/') + }) + }) +}) + +app.listen(3000) +``` + +## Debugging + +This module uses the [debug](https://www.npmjs.com/package/debug) module +internally to log information about session operations. + +To see all the internal logs, set the `DEBUG` environment variable to +`express-session` when launching your app (`npm start`, in this example): + +```sh +$ DEBUG=express-session npm start +``` + +On Windows, use the corresponding command; + +```sh +> set DEBUG=express-session & npm start ``` ## License [MIT](LICENSE) -[npm-image]: https://img.shields.io/npm/v/express-session.svg -[npm-url]: https://npmjs.org/package/express-session -[travis-image]: https://img.shields.io/travis/expressjs/session/master.svg -[travis-url]: https://travis-ci.org/expressjs/session -[coveralls-image]: https://img.shields.io/coveralls/expressjs/session/master.svg +[rfc-6265-5.1.4]: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 +[rfc-6265bis-03-4.1.2.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 +[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/ +[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1 +[ci-image]: https://badgen.net/github/checks/expressjs/session/master?label=ci +[ci-url]: https://github.com/expressjs/session/actions?query=workflow%3Aci +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/session/master [coveralls-url]: https://coveralls.io/r/expressjs/session?branch=master -[downloads-image]: https://img.shields.io/npm/dm/express-session.svg -[downloads-url]: https://npmjs.org/package/express-session -[gratipay-image]: https://img.shields.io/gratipay/dougwilson.svg -[gratipay-url]: https://gratipay.com/dougwilson/ +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/express-session +[npm-url]: https://npmjs.org/package/express-session +[npm-version-image]: https://badgen.net/npm/v/express-session diff --git a/index.js b/index.js index 01752a12..3e23e901 100644 --- a/index.js +++ b/index.js @@ -13,19 +13,20 @@ * @private */ +var Buffer = require('safe-buffer').Buffer var cookie = require('cookie'); -var crc = require('crc').crc32; +var crypto = require('crypto') var debug = require('debug')('express-session'); var deprecate = require('depd')('express-session'); +var onHeaders = require('on-headers') var parseUrl = require('parseurl'); +var signature = require('cookie-signature') var uid = require('uid-safe').sync - , onHeaders = require('on-headers') - , signature = require('cookie-signature') +var Cookie = require('./session/cookie') +var MemoryStore = require('./session/memory') var Session = require('./session/session') - , MemoryStore = require('./session/memory') - , Cookie = require('./session/cookie') - , Store = require('./session/store') +var Store = require('./session/store') // environment @@ -69,7 +70,7 @@ var defer = typeof setImmediate === 'function' * Setup session store with the given `options`. * * @param {Object} [options] - * @param {Object} [options.cookie] Options for cookie + * @param {Object|Function} [options.cookie] Options for cookie * @param {Function} [options.genid] * @param {String} [options.name=connect.sid] Session ID cookie name * @param {Boolean} [options.proxy] @@ -148,8 +149,8 @@ function session(options) { // notify user that this store is not // meant for a production environment - if ('production' == env && store instanceof MemoryStore) { - /* istanbul ignore next: not tested */ + /* istanbul ignore next: not tested */ + if (env === 'production' && store instanceof MemoryStore) { console.warn(warning); } @@ -157,10 +158,17 @@ function session(options) { store.generate = function(req){ req.sessionID = generateId(req); req.session = new Session(req); - req.session.cookie = new Cookie(cookieOptions); + var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions; + req.session.cookie = new Cookie(resolvedCookieOptions); - if (cookieOptions.secure === 'auto') { - req.session.cookie.secure = issecure(req, trustProxy); + var isSecure = issecure(req, trustProxy); + + if (resolvedCookieOptions.secure === 'auto') { + req.session.cookie.secure = isSecure; + } + + if (resolvedCookieOptions.sameSite === 'auto') { + req.session.cookie.sameSite = isSecure ? 'none' : 'lax'; } }; @@ -191,8 +199,16 @@ function session(options) { } // pathname mismatch - var originalPath = parseUrl.original(req).pathname; - if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next(); + var originalPath = parseUrl.original(req).pathname || '/' + var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions + var cfgPath = resolvedCookieOptions.path || '/' + + if (!rfcPathMatch(originalPath, cfgPath)) { + debug('pathname mismatch') + next() + return + } + // ensure a secret is available or bail if (!secret && !req.secret) { @@ -239,7 +255,11 @@ function session(options) { } // set cookie - setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data); + try { + setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data) + } catch (err) { + defer(next, err) + } }); // proxy end() to commit the session @@ -271,6 +291,10 @@ function session(options) { return ret; } + if (!res._header) { + res._implicitHeader() + } + if (chunk == null) { ret = true; return ret; @@ -281,7 +305,7 @@ function session(options) { if (!isNaN(contentLength) && contentLength > 0) { // measure chunk chunk = !Buffer.isBuffer(chunk) - ? new Buffer(chunk, encoding) + ? Buffer.from(chunk, encoding) : chunk; encoding = undefined; @@ -362,6 +386,29 @@ function session(options) { wrapmethods(req.session); } + // inflate the session + function inflate (req, sess) { + store.createSession(req, sess) + originalId = req.sessionID + originalHash = hash(sess) + + if (!resaveSession) { + savedHash = originalHash + } + + wrapmethods(req.session) + } + + function rewrapmethods (sess, callback) { + return function () { + if (req.session !== sess) { + wrapmethods(req.session) + } + + callback.apply(this, arguments) + } + } + // wrap session methods function wrapmethods(sess) { var _reload = sess.reload @@ -369,10 +416,7 @@ function session(options) { function reload(callback) { debug('reloading %s', this.id) - _reload.call(this, function () { - wrapmethods(req.session) - callback.apply(this, arguments) - }) + _reload.call(this, rewrapmethods(this, callback)) } function save() { @@ -419,7 +463,7 @@ function session(options) { return false; } - return !saveUninitializedSession && cookieId !== req.sessionID + return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID ? isModified(req.session) : !isSaved(req.session) } @@ -442,7 +486,7 @@ function session(options) { return false; } - return cookieId != req.sessionID + return cookieId !== req.sessionID ? saveUninitializedSession || isModified(req.session) : rollingSessions || req.session.cookie.expires != null && isModified(req.session); } @@ -459,38 +503,66 @@ function session(options) { debug('fetching %s', req.sessionID); store.get(req.sessionID, function(err, sess){ // error handling - if (err) { + if (err && err.code !== 'ENOENT') { debug('error %j', err); + next(err) + return + } - if (err.code !== 'ENOENT') { - next(err); - return; - } - - generate(); - // no session - } else if (!sess) { - debug('no session found'); - generate(); - // populate req.session - } else { - debug('session found'); - store.createSession(req, sess); - originalId = req.sessionID; - originalHash = hash(sess); - - if (!resaveSession) { - savedHash = originalHash + try { + if (err || !sess) { + debug('no session found') + generate() + } else { + debug('session found') + inflate(req, sess) } - - wrapmethods(req.session); + } catch (e) { + next(e) + return } - next(); + next() }); }; }; +/** + * Check if the cookiePath matches the requestPath following the + * rules in RFC 6265 section 5.1.4. + * + * @param {String} requestPath + * @param {String} cookiePath + * @return {Boolean} + * @private + */ + +function rfcPathMatch(requestPath, cookiePath) { + // Normalize inputs (Node 0.8-safe) + requestPath = (typeof requestPath === 'string' && requestPath.length) ? requestPath : '/'; + cookiePath = (typeof cookiePath === 'string' && cookiePath.length) ? cookiePath : '/'; + + // Root cookie matches everything + if (cookiePath === '/') return true; + + // Exact match + if (requestPath === cookiePath) return true; + + // Prefix match + if (requestPath.indexOf(cookiePath) === 0) { + // If cookiePath ends with '/', any longer requestPath is OK + if (cookiePath.charAt(cookiePath.length - 1) === '/') return true; + + // Otherwise the next char after the prefix must be '/' + var nextChar = requestPath.length > cookiePath.length + ? requestPath.charAt(cookiePath.length) + : ''; + return nextChar === '/'; + } + + return false; +} + /** * Generate a session ID for a new session. * @@ -498,7 +570,7 @@ function session(options) { * @private */ -function generateSessionId(sess) { +function generateSessionId() { return uid(24); } @@ -521,7 +593,7 @@ function getcookie(req, name, secrets) { raw = cookies[name]; if (raw) { - if (raw.substr(0, 2) === 's:') { + if (raw.slice(0, 2) === 's:') { val = unsigncookie(raw.slice(2), secrets); if (val === false) { @@ -548,7 +620,7 @@ function getcookie(req, name, secrets) { raw = req.cookies[name]; if (raw) { - if (raw.substr(0, 2) === 's:') { + if (raw.slice(0, 2) === 's:') { val = unsigncookie(raw.slice(2), secrets); if (val) { @@ -577,11 +649,21 @@ function getcookie(req, name, secrets) { */ function hash(sess) { - return crc(JSON.stringify(sess, function (key, val) { - if (key !== 'cookie') { - return val; + // serialize + var str = JSON.stringify(sess, function (key, val) { + // ignore sess.cookie property + if (this === sess && key === 'cookie') { + return } - })); + + return val + }) + + // hash + return crypto + .createHash('sha1') + .update(str, 'utf8') + .digest('hex') } /** @@ -595,7 +677,8 @@ function hash(sess) { function issecure(req, trustProxy) { // socket is https server - if (req.connection && req.connection.encrypted) { + var socket = req.socket || req.connection; + if (socket && socket.encrypted) { return true; } @@ -606,17 +689,14 @@ function issecure(req, trustProxy) { // no explicit trust; try req.secure from express if (trustProxy !== true) { - var secure = req.secure; - return typeof secure === 'boolean' - ? secure - : false; + return req.secure === true } // read the proto from x-forwarded-proto header var header = req.headers['x-forwarded-proto'] || ''; var index = header.indexOf(','); var proto = index !== -1 - ? header.substr(0, index).toLowerCase().trim() + ? header.slice(0, index).toLowerCase().trim() : header.toLowerCase().trim() return proto === 'https'; @@ -634,10 +714,10 @@ function setcookie(res, name, val, secret, options) { debug('set-cookie %s', data); - var prev = res.getHeader('set-cookie') || []; + var prev = res.getHeader('Set-Cookie') || [] var header = Array.isArray(prev) ? prev.concat(data) : [prev, data]; - res.setHeader('set-cookie', header) + res.setHeader('Set-Cookie', header) } /** diff --git a/package.json b/package.json index 5b0d8c9e..36387796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-session", - "version": "1.14.2", + "version": "1.19.0", "description": "Simple session middleware for Express", "author": "TJ Holowaychuk (http://tjholowaychuk.com)", "contributors": [ @@ -8,38 +8,43 @@ "Joe Wagner " ], "repository": "expressjs/session", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, "license": "MIT", "dependencies": { - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "crc": "3.4.1", - "debug": "2.3.2", - "depd": "~1.1.0", - "on-headers": "~1.0.1", - "parseurl": "~1.3.1", - "uid-safe": "~2.1.3", - "utils-merge": "1.0.0" + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" }, "devDependencies": { "after": "0.8.2", - "cookie-parser": "1.4.3", - "express": "4.14.0", - "istanbul": "0.4.5", - "mocha": "2.5.3", - "supertest": "1.1.0" + "cookie-parser": "1.4.6", + "eslint": "8.56.0", + "eslint-plugin-markdown": "3.0.1", + "express": "4.17.3", + "mocha": "10.8.2", + "nyc": "15.1.0", + "supertest": "6.3.4" }, "files": [ "session/", - "HISTORY.md", - "LICENSE", "index.js" ], "engines": { "node": ">= 0.8.0" }, "scripts": { - "test": "mocha --bail --reporter spec test/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot test/", - "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec test/" + "lint": "eslint . && node ./scripts/lint-readme.js", + "test": "./test/support/gencert.sh && mocha --require test/support/env --check-leaks --no-exit --reporter spec test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" } } diff --git a/scripts/lint-readme.js b/scripts/lint-readme.js new file mode 100644 index 00000000..f4c50d76 --- /dev/null +++ b/scripts/lint-readme.js @@ -0,0 +1,93 @@ +var assert = require('assert') +var fs = require('fs') +var path = require('path') + +var BADGE_STAR_LINK_REGEXP = /^https:\/\/badgen.net\/github\/stars\/[^/]+\/[^/]+?label=%E2%98%85$/ +var MARKDOWN_LINK_REGEXP = /^\[([^ \]]+)\]: ([^ ]+)$/ +var MARKDOWN_SECTION_REGEXP = /^#+ (.+)$/ +var NEWLINE_REGEXP = /\r?\n/ +var README_PATH = path.join(__dirname, '..', 'README.md') +var README_CONTENTS = fs.readFileSync(README_PATH, 'utf-8') +var STORE_HEADER_REGEXP = /^\[!\[★\]\[([^ \]]+)\] ([^ \]]+)\]\[([^ \]]+)\](?: .+)?$/ + +var header = null +var lintedStores = false +var section = null +var state = 0 + +README_CONTENTS.split(NEWLINE_REGEXP).forEach(function (line, lineidx) { + section = (MARKDOWN_SECTION_REGEXP.exec(line) || [])[1] || section + + if (section === 'Compatible Session Stores') { + lintedStores = true + + switch (state) { + case 0: // premble + if (line[0] !== '[') break + state = 1 + case 1: // header + var prev = header + + if (!(header = STORE_HEADER_REGEXP.exec(line))) { + expect(lineidx, 'session store header', line) + } else if (prev && prev[2].replace(/^[^/]+\//, '').localeCompare(header[2].replace(/^[^/]+\//, '')) > 0) { + expect(lineidx, (header[2] + ' to be alphabetically before ' + prev[2]), line) + state = 2 + } else { + state = 2 + } + + break + case 2: // blank line + if (line && MARKDOWN_LINK_REGEXP.test(line)) { + expect(lineidx, 'blank line or wrapped description', line) + } else if (line === '') { + state = 3 + } + break + case 3: // url link + var urlLink = MARKDOWN_LINK_REGEXP.exec(line) + + if (!urlLink) { + expect(lineidx, 'link reference', line) + } else if (urlLink[1] !== header[3]) { + expect(lineidx, ('link name of ' + header[3]), line) + } else if (urlLink[2] !== ('https://www.npmjs.com/package/' + header[2])) { + expect(lineidx, ('link url of https://www.npmjs.com/package/' + header[2]), line) + } else { + state = 4 + } + + break + case 4: // image link + var imageLink = MARKDOWN_LINK_REGEXP.exec(line) + + if (!imageLink) { + expect(lineidx, 'link reference', line) + } else if (imageLink[1] !== header[1]) { + expect(lineidx, ('link name of ' + header[1]), line) + } else if (!BADGE_STAR_LINK_REGEXP.test(imageLink[2])) { + expect(lineidx, ('link url to github stars badge'), line) + } else { + state = 5 + } + + break + case 5: // blank line + if (line !== '') { + expect(lineidx, 'blank line after links', line) + } else { + state = 1 + } + break + } + } +}) + +assert.ok(lintedStores, 'Compatible Session Stores section linted') + +function expect (lineidx, message, line) { + console.log('Expected %s on line %d', message, (lineidx + 1)) + console.log(' Got: %s', line) + process.exitCode = 1 +} diff --git a/scripts/version-history.js b/scripts/version-history.js new file mode 100644 index 00000000..c58268ad --- /dev/null +++ b/scripts/version-history.js @@ -0,0 +1,63 @@ +'use strict' + +var fs = require('fs') +var path = require('path') + +var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') +var MD_HEADER_REGEXP = /^====*$/ +var VERSION = process.env.npm_package_version +var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ + +var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') + +if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { + console.error('Missing header in HISTORY.md') + process.exit(1) +} + +if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { + console.error('Missing placeholder version in HISTORY.md') + process.exit(1) +} + +if (historyFileLines[0].indexOf('x') !== -1) { + var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') + + if (!versionCheckRegExp.test(VERSION)) { + console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) + process.exit(1) + } +} + +historyFileLines[0] = VERSION + ' / ' + getLocaleDate() +historyFileLines[1] = repeat('=', historyFileLines[0].length) + +fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) + +function getLocaleDate () { + var now = new Date() + + return zeroPad(now.getFullYear(), 4) + '-' + + zeroPad(now.getMonth() + 1, 2) + '-' + + zeroPad(now.getDate(), 2) +} + +function repeat (str, length) { + var out = '' + + for (var i = 0; i < length; i++) { + out += str + } + + return out +} + +function zeroPad (number, length) { + var num = number.toString() + + while (num.length < length) { + num = '0' + num + } + + return num +} diff --git a/session/cookie.js b/session/cookie.js index 60ef22af..8bb5907b 100644 --- a/session/cookie.js +++ b/session/cookie.js @@ -11,8 +11,8 @@ * Module dependencies. */ -var merge = require('utils-merge') - , cookie = require('cookie'); +var cookie = require('cookie') +var deprecate = require('depd')('express-session') /** * Initialize a new `Cookie` with the given `options`. @@ -26,10 +26,22 @@ var Cookie = module.exports = function Cookie(options) { this.path = '/'; this.maxAge = null; this.httpOnly = true; - if (options) merge(this, options); - this.originalMaxAge = undefined == this.originalMaxAge - ? this.maxAge - : this.originalMaxAge; + + if (options) { + if (typeof options !== 'object') { + throw new TypeError('argument options must be a object') + } + + for (var key in options) { + if (key !== 'data') { + this[key] = options[key] + } + } + } + + if (this.originalMaxAge === undefined || this.originalMaxAge === null) { + this.originalMaxAge = this.maxAge + } }; /*! @@ -69,7 +81,15 @@ Cookie.prototype = { */ set maxAge(ms) { - this.expires = 'number' == typeof ms + if (ms && typeof ms !== 'number' && !(ms instanceof Date)) { + throw new TypeError('maxAge must be a number or Date') + } + + if (ms instanceof Date) { + deprecate('maxAge as Date; pass number of milliseconds instead') + } + + this.expires = typeof ms === 'number' ? new Date(Date.now() + ms) : ms; }, @@ -96,7 +116,9 @@ Cookie.prototype = { get data() { return { - originalMaxAge: this.originalMaxAge + originalMaxAge: this.originalMaxAge, + partitioned: this.partitioned, + priority: this.priority , expires: this._expires , secure: this.secure , httpOnly: this.httpOnly diff --git a/session/memory.js b/session/memory.js index 25252b6c..11ed686c 100644 --- a/session/memory.js +++ b/session/memory.js @@ -171,14 +171,16 @@ function getSession(sessionId) { // parse sess = JSON.parse(sess) - var expires = typeof sess.cookie.expires === 'string' - ? new Date(sess.cookie.expires) - : sess.cookie.expires - - // destroy expired session - if (expires && expires <= Date.now()) { - delete this.sessions[sessionId] - return + if (sess.cookie) { + var expires = typeof sess.cookie.expires === 'string' + ? new Date(sess.cookie.expires) + : sess.cookie.expires + + // destroy expired session + if (expires && expires <= Date.now()) { + delete this.sessions[sessionId] + return + } } return sess diff --git a/session/session.js b/session/session.js index 2eacde60..fee7608c 100644 --- a/session/session.js +++ b/session/session.js @@ -87,7 +87,8 @@ defineMethod(Session.prototype, 'save', function save(fn) { defineMethod(Session.prototype, 'reload', function reload(fn) { var req = this.req - , store = this.req.sessionStore; + var store = this.req.sessionStore + store.get(this.id, function(err, sess){ if (err) return fn(err); if (!sess) return fn(new Error('failed to load session')); diff --git a/session/store.js b/session/store.js index 387469c5..3793877e 100644 --- a/session/store.js +++ b/session/store.js @@ -85,10 +85,18 @@ Store.prototype.load = function(sid, fn){ Store.prototype.createSession = function(req, sess){ var expires = sess.cookie.expires - , orig = sess.cookie.originalMaxAge; + var originalMaxAge = sess.cookie.originalMaxAge + sess.cookie = new Cookie(sess.cookie); - if ('string' == typeof expires) sess.cookie.expires = new Date(expires); - sess.cookie.originalMaxAge = orig; + + if (typeof expires === 'string') { + // convert expires to a Date object + sess.cookie.expires = new Date(expires) + } + + // keep originalMaxAge intact + sess.cookie.originalMaxAge = originalMaxAge + req.session = new Session(req, sess); return req.session; }; diff --git a/test/cookie.js b/test/cookie.js new file mode 100644 index 00000000..ea676e35 --- /dev/null +++ b/test/cookie.js @@ -0,0 +1,134 @@ + +var assert = require('assert') +var Cookie = require('../session/cookie') + +describe('new Cookie()', function () { + it('should create a new cookie object', function () { + assert.strictEqual(typeof new Cookie(), 'object') + }) + + it('should default expires to null', function () { + var cookie = new Cookie() + assert.strictEqual(cookie.expires, null) + }) + + it('should default httpOnly to true', function () { + var cookie = new Cookie() + assert.strictEqual(cookie.httpOnly, true) + }) + + it('should default path to "/"', function () { + var cookie = new Cookie() + assert.strictEqual(cookie.path, '/') + }) + + it('should default maxAge to null', function () { + var cookie = new Cookie() + assert.strictEqual(cookie.maxAge, null) + }) + + describe('with options', function () { + it('should create a new cookie object', function () { + assert.strictEqual(typeof new Cookie({}), 'object') + }) + + it('should reject non-objects', function () { + assert.throws(function () { new Cookie(42) }, /argument options/) + assert.throws(function () { new Cookie('foo') }, /argument options/) + assert.throws(function () { new Cookie(true) }, /argument options/) + assert.throws(function () { new Cookie(function () {}) }, /argument options/) + }) + + it('should ignore "data" option', function () { + var cookie = new Cookie({ data: { foo: 'bar' }, path: '/foo' }) + + assert.strictEqual(typeof cookie, 'object') + assert.strictEqual(typeof cookie.data, 'object') + assert.strictEqual(cookie.data.path, '/foo') + assert.notStrictEqual(cookie.data.foo, 'bar') + }) + + describe('expires', function () { + it('should set expires', function () { + var expires = new Date(Date.now() + 60000) + var cookie = new Cookie({ expires: expires }) + + assert.strictEqual(cookie.expires, expires) + }) + + it('should set maxAge', function () { + var expires = new Date(Date.now() + 60000) + var cookie = new Cookie({ expires: expires }) + + assert.ok(expires.getTime() - Date.now() - 1000 <= cookie.maxAge) + assert.ok(expires.getTime() - Date.now() + 1000 >= cookie.maxAge) + }) + }) + + describe('httpOnly', function () { + it('should set httpOnly', function () { + var cookie = new Cookie({ httpOnly: false }) + + assert.strictEqual(cookie.httpOnly, false) + }) + }) + + describe('maxAge', function () { + it('should set expires', function () { + var maxAge = 60000 + var cookie = new Cookie({ maxAge: maxAge }) + + assert.ok(cookie.expires.getTime() - Date.now() - 1000 <= maxAge) + assert.ok(cookie.expires.getTime() - Date.now() + 1000 >= maxAge) + }) + + it('should set maxAge', function () { + var maxAge = 60000 + var cookie = new Cookie({ maxAge: maxAge }) + + assert.strictEqual(typeof cookie.maxAge, 'number') + assert.ok(cookie.maxAge - 1000 <= maxAge) + assert.ok(cookie.maxAge + 1000 >= maxAge) + }) + + it('should accept Date object', function () { + var maxAge = new Date(Date.now() + 60000) + var cookie = new Cookie({ maxAge: maxAge }) + + assert.strictEqual(cookie.expires.getTime(), maxAge.getTime()) + assert.ok(maxAge.getTime() - Date.now() - 1000 <= cookie.maxAge) + assert.ok(maxAge.getTime() - Date.now() + 1000 >= cookie.maxAge) + }) + + it('should reject invalid types', function() { + assert.throws(function() { new Cookie({ maxAge: '42' }) }, /maxAge/) + assert.throws(function() { new Cookie({ maxAge: true }) }, /maxAge/) + assert.throws(function() { new Cookie({ maxAge: function () {} }) }, /maxAge/) + }) + }) + + describe('partitioned', function () { + it('should set partitioned', function () { + var cookie = new Cookie({ partitioned: true }) + + assert.strictEqual(cookie.partitioned, true) + }) + }) + + describe('path', function () { + it('should set path', function () { + var cookie = new Cookie({ path: '/foo' }) + + assert.strictEqual(cookie.path, '/foo') + }) + }) + + describe('priority', function () { + it('should set priority', function () { + var cookie = new Cookie({ priority: 'high' }) + + assert.strictEqual(cookie.priority, 'high') + }) + }) + }) +}) diff --git a/test/fixtures/.gitkeep b/test/fixtures/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/server.crt b/test/fixtures/server.crt deleted file mode 100644 index c019198a..00000000 --- a/test/fixtures/server.crt +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICMzCCAZwCCQCJTms0qcIZgDANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQGEwJV -UzEQMA4GA1UECBMHSW5kaWFuYTEdMBsGA1UEChMUbm9kZS1leHByZXNzLXNlc3Np -b24xHjAcBgNVBAMTFWV4cHJlc3Mtc2Vzc2lvbi5sb2NhbDAeFw0xNDExMjMwNTQ3 -MzlaFw0yNDExMjAwNTQ3MzlaMF4xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdJbmRp -YW5hMR0wGwYDVQQKExRub2RlLWV4cHJlc3Mtc2Vzc2lvbjEeMBwGA1UEAxMVZXhw -cmVzcy1zZXNzaW9uLmxvY2FsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDY -G398zqN6Yv/FAx77eoLLje4VNCYDXUciWBzceaZ5u/rtP/XshTQWDKFfUbb/kmni -DazsCasSoNsUCrcqQPJF1jF3F9XjsZxcggDab8MAbAMhnhJax2l3yPJsWnB/8mrY -cdsdQNPXJxQW9MMZgUxz73gIY5/rEeayU9owgcnVcQIDAQABMA0GCSqGSIb3DQEB -BQUAA4GBADi0XXsZlQAKxOyD4qvdJNWJXlfqhr1q53dZmfF7nikDaMDiMspczTS/ -pxNbq2UaMc7g+6qmXPaPQoN3laQQtMdyVfSh6EIJHbzXXzdcCT4fDBYX7iwvh0Gg -DUpjmxmCzFCaob9+hZzwbJi5MFQ4Qq12LW1aYHMs8wgLboHml1WL ------END CERTIFICATE----- diff --git a/test/fixtures/server.key b/test/fixtures/server.key deleted file mode 100644 index dabf2b83..00000000 --- a/test/fixtures/server.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDYG398zqN6Yv/FAx77eoLLje4VNCYDXUciWBzceaZ5u/rtP/Xs -hTQWDKFfUbb/kmniDazsCasSoNsUCrcqQPJF1jF3F9XjsZxcggDab8MAbAMhnhJa -x2l3yPJsWnB/8mrYcdsdQNPXJxQW9MMZgUxz73gIY5/rEeayU9owgcnVcQIDAQAB -AoGBALB8HGw/kPA1Ay3Qc6/qCADWYvW8BcM/nQUmMkO3sUW/R5gTYPIMglHzdKIU -aL9kwcXTZ0HIT4ZCCUffzF/cdD0lqCZjwGl0aM4xUZcPbaM/KmZOUcP92ymsN+rF -uuJXks6hxrmJ5hh5D6FXdlQjTCdG3u9w0+KD4n1BkBkEaqflAkEA9q4TBt1bLOvV -bbwz7bunI6bwS5eD7sVn1qUqrm6hcirhF1xCv75pO/uqAyccbguxjp8y2LNuN+cO -IgSwr+Jq1wJBAOBFuKSMAcg2WmJ4IAT7143yhoHKMgA8Nl4sfLFwFl2/hw+fdEBe -gndRfKHT4IV/YLcnS7d/H6mEMSAusWx5QPcCQQDTYaF+TWrW2JRAf3jEK/xyiZf6 -PrDYh6KOhWRIqxZ/fYz69p1gL6t/sg0ivH4ZMr4JKBRrK360OrOapQg+/7drAkB6 -3pfPRok/aE/SfN+F+3fX49Q/TUhhipt6ssLJ74/BYsobDBADqAOwXSt7+XmbifKx -xUydRn9RPwQvDoXT2QZ3AkAdnwR9PMEHAsaibrPyzBztKjPL2rWBRk1QAmcdrTpt -XL99XfmkERWtiBA/Lz2K332qYOa/zo5c/SADx9fm7+HP ------END RSA PRIVATE KEY----- diff --git a/test/session.js b/test/session.js index 9cfb6ef9..405e3ddf 100644 --- a/test/session.js +++ b/test/session.js @@ -1,37 +1,38 @@ -process.env.NO_DEPRECATION = 'express-session'; - var after = require('after') var assert = require('assert') +var cookieParser = require('cookie-parser') +var crypto = require('crypto') var express = require('express') - , request = require('supertest') - , cookieParser = require('cookie-parser') - , session = require('../') - , Cookie = require('../session/cookie') var fs = require('fs') var http = require('http') var https = require('https') -var util = require('util') +var request = require('supertest') +var session = require('../') +var SmartStore = require('./support/smart-store') +var SyncStore = require('./support/sync-store') +var utils = require('./support/utils') + +var Cookie = require('../session/cookie') var min = 60 * 1000; describe('session()', function(){ it('should export constructors', function(){ - assert.equal(typeof session.Session, 'function') - assert.equal(typeof session.Store, 'function') - assert.equal(typeof session.MemoryStore, 'function') + assert.strictEqual(typeof session.Session, 'function') + assert.strictEqual(typeof session.Store, 'function') + assert.strictEqual(typeof session.MemoryStore, 'function') }) it('should do nothing if req.session exists', function(done){ - var app = express() - .use(function(req, res, next){ req.session = {}; next(); }) - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) - .use(end); + function setup (req) { + req.session = {} + } - request(app) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) + request(createServer(setup)) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }) it('should error without secret', function(done){ @@ -41,13 +42,11 @@ describe('session()', function(){ }) it('should get secret from req.secret', function(done){ - var app = express() - .use(function(req, res, next){ req.secret = 'keyboard cat'; next(); }) - .use(session({ cookie: { maxAge: min }})) - .use(end); - app.set('env', 'test'); + function setup (req) { + req.secret = 'keyboard cat' + } - request(app) + request(createServer(setup, { secret: undefined })) .get('/') .expect(200, '', done) }) @@ -66,7 +65,7 @@ describe('session()', function(){ if (err) return done(err) store.length(function (err, len) { if (err) return done(err) - assert.equal(len, 1) + assert.strictEqual(len, 1) done() }) }) @@ -153,7 +152,7 @@ describe('session()', function(){ if (err) return done(err) store.all(function (err, sess) { if (err) return done(err) - assert.equal(Object.keys(sess).length, 2) + assert.strictEqual(Object.keys(sess).length, 2) done() }) } @@ -167,17 +166,25 @@ describe('session()', function(){ .expect(200, 'session created', cb) }) + it('should handle empty req.url', function (done) { + function setup (req) { + req.url = '' + } + + request(createServer(setup)) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + it('should handle multiple res.end calls', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) - .use(function(req, res){ - res.setHeader('Content-Type', 'text/plain'); - res.end('Hello, world!'); - res.end(); - }); - app.set('env', 'test'); + var server = createServer(null, function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('Hello, world!') + res.end() + }) - request(app) + request(server) .get('/') .expect('Content-Type', 'text/plain') .expect(200, 'Hello, world!', done); @@ -250,35 +257,33 @@ describe('session()', function(){ if (err) return done(err) store.length(function (err, length) { if (err) return done(err) - assert.equal(length, 0) + assert.strictEqual(length, 0) done() }) }) }) it('should update cookie expiration when slow write', function (done) { - var app = express(); - app.use(session({ rolling: true, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function (req, res, next) { - req.session.user = 'bob'; - res.write('hello, '); + var server = createServer({ rolling: true }, function (req, res) { + req.session.user = 'bob' + res.write('hello, ') setTimeout(function () { - res.end('world!'); - }, 200); - }); + res.end('world!') + }, 200) + }) - request(app) + request(server) .get('/') .expect(shouldSetCookie('connect.sid')) .expect(200, function (err, res) { if (err) return done(err); var originalExpires = expires(res); setTimeout(function () { - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) .expect(shouldSetCookie('connect.sid')) - .expect(function (res) { assert.notEqual(originalExpires, expires(res)); }) + .expect(function (res) { assert.notStrictEqual(originalExpires, expires(res)); }) .expect(200, done); }, (1000 - (Date.now() % 1000) + 200)); }); @@ -286,34 +291,21 @@ describe('session()', function(){ describe('when response ended', function () { it('should have saved session', function (done) { - var saved = false var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true res.end('session saved') }) - var _set = store.set - store.set = function set(sid, sess, callback) { - setTimeout(function () { - _set.call(store, sid, sess, function (err) { - saved = true - callback(err) - }) - }, 200) - } - request(server) - .get('/') - .expect(200, 'session saved', function (err) { - if (err) return done(err) - assert.ok(saved) - done() - }) + .get('/') + .expect(200) + .expect(shouldSetSessionInStore(store, 200)) + .expect('session saved') + .end(done) }) it('should have saved session even with empty response', function (done) { - var saved = false var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true @@ -321,27 +313,14 @@ describe('session()', function(){ res.end() }) - var _set = store.set - store.set = function set(sid, sess, callback) { - setTimeout(function () { - _set.call(store, sid, sess, function (err) { - saved = true - callback(err) - }) - }, 200) - } - request(server) - .get('/') - .expect(200, '', function (err) { - if (err) return done(err) - assert.ok(saved) - done() - }) + .get('/') + .expect(200) + .expect(shouldSetSessionInStore(store, 200)) + .end(done) }) it('should have saved session even with multi-write', function (done) { - var saved = false var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true @@ -350,27 +329,15 @@ describe('session()', function(){ res.end('world') }) - var _set = store.set - store.set = function set(sid, sess, callback) { - setTimeout(function () { - _set.call(store, sid, sess, function (err) { - saved = true - callback(err) - }) - }, 200) - } - request(server) - .get('/') - .expect(200, 'hello, world', function (err) { - if (err) return done(err) - assert.ok(saved) - done() - }) + .get('/') + .expect(200) + .expect(shouldSetSessionInStore(store, 200)) + .expect('hello, world') + .end(done) }) it('should have saved session even with non-chunked response', function (done) { - var saved = false var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true @@ -378,23 +345,12 @@ describe('session()', function(){ res.end('session saved') }) - var _set = store.set - store.set = function set(sid, sess, callback) { - setTimeout(function () { - _set.call(store, sid, sess, function (err) { - saved = true - callback(err) - }) - }, 200) - } - request(server) - .get('/') - .expect(200, 'session saved', function (err) { - if (err) return done(err) - assert.ok(saved) - done() - }) + .get('/') + .expect(200) + .expect(shouldSetSessionInStore(store, 200)) + .expect('session saved') + .end(done) }) it('should have saved session with updated cookie expiration', function (done) { @@ -414,7 +370,7 @@ describe('session()', function(){ if (err) return done(err) assert.ok(sess, 'session saved to store') var exp = new Date(sess.cookie.expires) - assert.equal(exp.toUTCString(), expires(res)) + assert.strictEqual(exp.toUTCString(), expires(res)) setTimeout(function () { request(server) .get('/') @@ -423,9 +379,9 @@ describe('session()', function(){ if (err) return done(err) store.get(id, function (err, sess) { if (err) return done(err) - assert.equal(res.text, id) + assert.strictEqual(res.text, id) assert.ok(sess, 'session still in store') - assert.notEqual(new Date(sess.cookie.expires).toUTCString(), exp.toUTCString(), 'session cookie expiration updated') + assert.notStrictEqual(new Date(sess.cookie.expires).toUTCString(), exp.toUTCString(), 'session cookie expiration updated') done() }) }) @@ -472,19 +428,14 @@ describe('session()', function(){ .expect(shouldSetCookie('connect.sid')) .expect(200, 'session 1', function (err, res) { if (err) return done(err) - var val = sid(res) - assert.ok(val) store.clear(function (err) { if (err) return done(err) request(server) .get('/') .set('Cookie', cookie(res)) .expect(shouldSetCookie('connect.sid')) - .expect(200, 'session 2', function (err, res) { - if (err) return done(err) - assert.notEqual(sid(res), val) - done() - }) + .expect(shouldSetCookieToDifferentSessionId(sid(res))) + .expect(200, 'session 2', done) }) }) }) @@ -510,6 +461,7 @@ describe('session()', function(){ .get('/') .set('Cookie', 'sessid=' + val) .expect(shouldSetCookie('sessid')) + .expect(shouldSetCookieToDifferentSessionId(val)) .expect(200, 'session created', done) }) }) @@ -576,18 +528,13 @@ describe('session()', function(){ .expect(shouldSetCookie('connect.sid')) .expect(200, 'session 1', function (err, res) { if (err) return done(err) - var val = sid(res) - assert.ok(val) setTimeout(function () { request(server) .get('/') .set('Cookie', cookie(res)) .expect(shouldSetCookie('connect.sid')) - .expect(200, 'session 2', function (err, res) { - if (err) return done(err) - assert.notEqual(sid(res), val) - done() - }) + .expect(shouldSetCookieToDifferentSessionId(sid(res))) + .expect(200, 'session 2', done) }, 15) }) }) @@ -608,7 +555,7 @@ describe('session()', function(){ setTimeout(function () { store.all(function (err, sess) { if (err) return done(err) - assert.equal(Object.keys(sess).length, 0) + assert.strictEqual(Object.keys(sess).length, 0) done() }) }, 10) @@ -616,6 +563,31 @@ describe('session()', function(){ }) }) + describe('when session without cookie property in store', function () { + it('should pass error from inflate', function (done) { + var count = 0 + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.session.num = req.session.num || ++count + res.end('session ' + req.session.num) + }) + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'session 1', function (err, res) { + if (err) return done(err) + store.set(sid(res), { foo: 'bar' }, function (err) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(500, /Cannot read prop/, done) + }) + }) + }) + }) + describe('proxy option', function(){ describe('when enabled', function(){ var server @@ -648,56 +620,66 @@ describe('session()', function(){ }) describe('when disabled', function(){ - var server before(function () { - server = createServer({ proxy: false, cookie: { secure: true, maxAge: 5 }}) + function setup (req) { + req.secure = req.headers['x-secure'] + ? JSON.parse(req.headers['x-secure']) + : undefined + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { proxy: false, cookie: { secure: true }}, respond) }) it('should not trust X-Forwarded-Proto', function(done){ - request(server) + request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, done) }) - it('should ignore req.secure from express', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', proxy: false, cookie: { secure: true, maxAge: min }})) - .use(function(req, res) { res.json(req.secure); }); - app.enable('trust proxy'); - - request(app) + it('should ignore req.secure', function (done) { + request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') + .set('X-Secure', 'true') .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, 'true', done) }) }) describe('when unspecified', function(){ - var server before(function () { - server = createServer({ cookie: { secure: true, maxAge: 5 }}) + function setup (req) { + req.secure = req.headers['x-secure'] + ? JSON.parse(req.headers['x-secure']) + : undefined + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { secure: true }}, respond) }) it('should not trust X-Forwarded-Proto', function(done){ - request(server) + request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, done) }) - it('should use req.secure from express', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { secure: true, maxAge: min }})) - .use(function(req, res) { res.json(req.secure); }); - app.enable('trust proxy'); - - request(app) + it('should use req.secure', function (done) { + request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') + .set('X-Secure', 'true') .expect(shouldSetCookie('connect.sid')) .expect(200, 'true', done) }) @@ -705,6 +687,61 @@ describe('session()', function(){ }) describe('cookie option', function () { + describe('when "path" set to "/foo/bar"', function () { + before(function () { + this.server = createServer({ cookie: { path: '/foo/bar' } }) + }) + + it('should not set cookie for "/" request', function (done) { + request(this.server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + + it('should not set cookie for "http://foo/bar" request', function (done) { + request(this.server) + .get('/') + .set('host', 'http://foo/bar') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + + it('should set cookie for "/foo/bar" request', function (done) { + request(this.server) + .get('/foo/bar/baz') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set cookie for "/foo/bar/baz" request', function (done) { + request(this.server) + .get('/foo/bar/baz') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + describe('when mounted at "/foo"', function () { + before(function () { + this.server = createServer(mountAt('/foo'), { cookie: { path: '/foo/bar' } }) + }) + + it('should set cookie for "/foo/bar" request', function (done) { + request(this.server) + .get('/foo/bar') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should not set cookie for "/foo/foo/bar" request', function (done) { + request(this.server) + .get('/foo/foo/bar') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + }) + }) + describe('when "secure" set to "auto"', function () { describe('when "proxy" is "true"', function () { before(function () { @@ -715,8 +752,7 @@ describe('session()', function(){ request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') - .expect(shouldSetCookie('connect.sid')) - .expect(shouldSetSecureCookie('connect.sid')) + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) .expect(200, done) }) }) @@ -730,433 +766,670 @@ describe('session()', function(){ request(this.server) .get('/') .set('X-Forwarded-Proto', 'https') - .expect(shouldSetCookie('connect.sid')) - .expect(shouldNotSetSecureCookie('connect.sid')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) .expect(200, done) }) }) describe('when "proxy" is undefined', function() { before(function () { - this.app = express() - .use(function(req, res, next) { Object.defineProperty(req, 'secure', { value: JSON.parse(req.headers['x-secure']) }); next(); }) - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min, secure: 'auto' }})) - .use(function(req, res) { res.json(req.secure); }); + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { secure: 'auto' } }, respond) }) it('should set secure if req.secure = true', function (done) { - request(this.app) + request(this.server) .get('/') .set('X-Secure', 'true') - .expect(shouldSetCookie('connect.sid')) - .expect(shouldSetSecureCookie('connect.sid')) + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) .expect(200, 'true', done) }) it('should not set secure if req.secure = false', function (done) { - request(this.app) + request(this.server) .get('/') .set('X-Secure', 'false') - .expect(shouldSetCookie('connect.sid')) - .expect(shouldNotSetSecureCookie('connect.sid')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) .expect(200, 'false', done) }) }) - }) - }) - - describe('genid option', function(){ - it('should reject non-function values', function(){ - assert.throws(session.bind(null, { genid: 'bogus!' }), /genid.*must/) - }); - - it('should provide default generator', function(done){ - request(createServer()) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }); - - it('should allow custom function', function(done){ - function genid() { return 'apple' } - - request(createServer({ genid: genid })) - .get('/') - .expect(shouldSetCookieToValue('connect.sid', 's%3Aapple.D8Y%2BpkTAmeR0PobOhY4G97PRW%2Bj7bUnP%2F5m6%2FOn1MCU')) - .expect(200, done) - }); - it('should encode unsafe chars', function(done){ - function genid() { return '%' } - - request(createServer({ genid: genid })) - .get('/') - .expect(shouldSetCookieToValue('connect.sid', 's%3A%25.kzQ6x52kKVdF35Qh62AWk4ZekS28K5XYCXKa%2FOTZ01g')) - .expect(200, done) - }); + describe('when request socket is encrypted', function () { + it('should set secure over TLS', function (done) { + var cert = fs.readFileSync(__dirname + '/fixtures/server.crt', 'ascii') + var server = https.createServer({ + key: fs.readFileSync(__dirname + '/fixtures/server.key', 'ascii'), + cert: cert + }) - it('should provide req argument', function(done){ - function genid(req) { return req.url } + server.on('request', createRequestListener({ secret: 'keyboard cat', cookie: { secure: 'auto' } })) - request(createServer({ genid: genid })) - .get('/foo') - .expect(shouldSetCookieToValue('connect.sid', 's%3A%2Ffoo.paEKBtAHbV5s1IB8B2zPnzAgYmmnRPIqObW4VRYj%2FMQ')) - .expect(200, done) - }); - }); + var agent = new https.Agent({ ca: cert }) + var createConnection = agent.createConnection - describe('key option', function(){ - it('should default to "connect.sid"', function(done){ - request(createServer()) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }) + agent.createConnection = function (options) { + options.servername = 'express-session.local' + return createConnection.call(this, options) + } - it('should allow overriding', function(done){ - request(createServer({ key: 'session_id' })) - .get('/') - .expect(shouldSetCookie('session_id')) - .expect(200, done) + var req = request(server).get('/') + req.agent(agent) + req.expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) + req.expect(200, done) + }) + }) }) - }) - describe('rolling option', function(){ - it('should default to false', function(done){ - var app = express(); - app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - req.session.user = 'bob'; - res.end(); + describe('when "cookie" is a function', function () { + it('should call custom function and apply cookie options', function (done) { + var cookieCallbackCalled = false; + var cookieCallback = function () { + cookieCallbackCalled = true; + return { path: '/test', httpOnly: true, secure: false }; + }; + var server = createServer({ cookie: cookieCallback }); + request(server) + .get('/test') + .expect( + shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', '/test') + ) + .expect(shouldSetCookieWithAttribute('connect.sid', 'HttpOnly')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(200, function (err) { + if (err) return done(err); + assert.strictEqual( + cookieCallbackCalled, + true, + 'should have called cookie callback' + ); + done(); + }); }); - request(app) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, function(err, res){ - if (err) return done(err); - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) + it('should provide req argument', function (done) { + var _path = '/test'; + var cookieCallbackCalled = false; + var cookieCallback = function (req) { + cookieCallbackCalled = true; + return { path: req.url, httpOnly: true, secure: false }; + }; + var server = createServer({ cookie: cookieCallback }); + request(server) + .get(_path) + .expect( + shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', _path) + ) + .expect(shouldSetCookieWithAttribute('connect.sid', 'HttpOnly')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(200, function (err) { + if (err) return done(err); + assert.strictEqual( + cookieCallbackCalled, + true, + 'should have called cookie callback' + ); + done(); + }); }); - }); - it('should force cookie on unmodified session', function(done){ - var app = express(); - app.use(session({ rolling: true, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - req.session.user = 'bob'; - res.end(); + it('should not set cookie when request-path does not match returned path', function (done) { + var cookieCallback = function () { + return { path: '/admin', httpOnly: true, secure: false }; + }; + var server = createServer({ cookie: cookieCallback }); + request(server) + .get('/administrator') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done); }); - request(app) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, function(err, res){ - if (err) return done(err); - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }); - }); + it('should resolve "secure" and "sameSite" set to "auto"', function (done) { + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } - it('should not force cookie on uninitialized session if saveUninitialized option is set to false', function(done){ - var count = 0; - var app = express(); - app.use(session({ rolling: true, saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); + function respond (req, res) { + res.end(String(req.secure)) + } + + var cookieCallback = function () { + return { path: '/', secure: 'auto', sameSite: 'auto' }; }; - res.end(); + var server = createServer(setup, { cookie: cookieCallback }, respond); + request(server) + .get('/') + .set('X-Secure', 'true') + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) + .expect( + shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None') + ) + .expect(200, 'true', done); }); + }); - request(app) + + describe('when "sameSite" set to "auto"', function () { + describe('basic functionality', function () { + before(function () { + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { sameSite: 'auto' } }, respond) + }) + + it('should set SameSite=None for secure connections', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'true') + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')) + .expect(200, 'true', done) + }) + + it('should set SameSite=Lax for insecure connections', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'false') + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax')) + .expect(200, 'false', done) + }) + }) + + describe('with proxy settings', function () { + describe('when "proxy" is "true"', function () { + before(function () { + this.server = createServer({ proxy: true, cookie: { sameSite: 'auto' }}) + }) + + it('should set SameSite=None when X-Forwarded-Proto is https', function (done) { + request(this.server) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')) + .expect(200, done) + }) + + it('should set SameSite=Lax when X-Forwarded-Proto is http', function (done) { + request(this.server) + .get('/') + .set('X-Forwarded-Proto', 'http') + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax')) + .expect(200, done) + }) + }) + + describe('when "proxy" is "false"', function () { + before(function () { + this.server = createServer({ proxy: false, cookie: { sameSite: 'auto' }}) + }) + + it('should set SameSite=Lax when X-Forwarded-Proto is https', function (done) { + request(this.server) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax')) + .expect(200, done) + }) + }) + }) + + describe('combined with secure auto', function() { + describe('when "secure" is "auto"', function () { + before(function () { + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { secure: 'auto', sameSite: 'auto' } }, respond) + }) + + it('should set both Secure and SameSite=None when secure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'true') + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')) + .expect(200, 'true', done) + }) + + it('should set neither Secure nor SameSite=None when insecure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'false') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax')) + .expect(200, 'false', done) + }) + }) + + describe('when "secure" is "false"', function () { + before(function () { + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { secure: false, sameSite: 'auto' } }, respond) + }) + + it('should set SameSite=None without Secure when secure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'true') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')) + .expect(200, 'true', done) + }) + + it('should set SameSite=Lax without Secure when insecure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'false') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax')) + .expect(200, 'false', done) + }) + }) + + describe('when "secure" is "true"', function () { + before(function () { + function setup (req) { + req.secure = JSON.parse(req.headers['x-secure']) + } + + function respond (req, res) { + res.end(String(req.secure)) + } + + this.server = createServer(setup, { cookie: { secure: true, sameSite: 'auto' } }, respond) + }) + + it('should set both Secure and SameSite=None when secure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'true') + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')) + .expect(200, 'true', done) + }) + + it('should not set cookie when insecure', function (done) { + request(this.server) + .get('/') + .set('X-Secure', 'false') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 'false', done) + }) + }) + }) + }) + }) + + describe('genid option', function(){ + it('should reject non-function values', function(){ + assert.throws(session.bind(null, { genid: 'bogus!' }), /genid.*must/) + }); + + it('should provide default generator', function(done){ + request(createServer()) .get('/') - .expect('x-count', '0') - .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(shouldSetCookie('connect.sid')) .expect(200, done) }); - it('should force cookie and save uninitialized session if saveUninitialized option is set to true', function(done){ - var count = 0; - var app = express(); - app.use(session({ rolling: true, saveUninitialized: true, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + it('should allow custom function', function(done){ + function genid() { return 'apple' } - request(app) + request(createServer({ genid: genid })) .get('/') - .expect('x-count', '1') - .expect(shouldSetCookie('connect.sid')) + .expect(shouldSetCookieToValue('connect.sid', 's%3Aapple.D8Y%2BpkTAmeR0PobOhY4G97PRW%2Bj7bUnP%2F5m6%2FOn1MCU')) .expect(200, done) }); - it('should force cookie and save modified session even if saveUninitialized option is set to false', function(done){ - var count = 0; - var app = express(); - app.use(session({ rolling: true, saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.count = count; - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + it('should encode unsafe chars', function(done){ + function genid() { return '%' } - request(app) + request(createServer({ genid: genid })) .get('/') - .expect('x-count', '1') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done); + .expect(shouldSetCookieToValue('connect.sid', 's%3A%25.kzQ6x52kKVdF35Qh62AWk4ZekS28K5XYCXKa%2FOTZ01g')) + .expect(200, done) + }); + + it('should provide req argument', function(done){ + function genid(req) { return req.url } + + request(createServer({ genid: genid })) + .get('/foo') + .expect(shouldSetCookieToValue('connect.sid', 's%3A%2Ffoo.paEKBtAHbV5s1IB8B2zPnzAgYmmnRPIqObW4VRYj%2FMQ')) + .expect(200, done) }); }); - describe('resave option', function(){ - it('should default to true', function(done){ - var count = 0; - var app = express(); - app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + describe('key option', function(){ + it('should default to "connect.sid"', function(done){ + request(createServer()) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) - request(app) + it('should allow overriding', function(done){ + request(createServer({ key: 'session_id' })) + .get('/') + .expect(shouldSetCookie('session_id')) + .expect(200, done) + }) + }) + + describe('name option', function () { + it('should default to "connect.sid"', function (done) { + request(createServer()) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set the cookie name', function (done) { + request(createServer({ name: 'session_id' })) + .get('/') + .expect(shouldSetCookie('session_id')) + .expect(200, done) + }) + }) + + describe('rolling option', function(){ + it('should default to false', function(done){ + var server = createServer(null, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetCookie('connect.sid')) .expect(200, function(err, res){ if (err) return done(err); - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) - .expect('x-count', '2') - .expect(200, done); + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }); }); - it('should force save on unmodified session', function(done){ - var count = 0; - var app = express(); - app.use(session({ resave: true, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + it('should force cookie on unmodified session', function(done){ + var server = createServer({ rolling: true }, function (req, res) { + req.session.user = 'bob' + res.end() + }) - request(app) + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetCookie('connect.sid')) .expect(200, function(err, res){ if (err) return done(err); - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) - .expect('x-count', '2') - .expect(200, done); + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) }); }); - it('should prevent save on unmodified session', function(done){ - var count = 0; - var app = express(); - app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + it('should not force cookie on uninitialized session if saveUninitialized option is set to false', function(done){ + var store = new session.MemoryStore() + var server = createServer({ store: store, rolling: true, saveUninitialized: false }) - request(app) + request(server) .get('/') - .expect('x-count', '1') - .expect(200, function(err, res){ - if (err) return done(err); - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect('x-count', '1') - .expect(200, done); - }); + .expect(shouldNotSetSessionInStore(store)) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }); - it('should still save modified session', function(done){ - var count = 0; - var app = express(); - app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.count = count; - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + it('should force cookie and save uninitialized session if saveUninitialized option is set to true', function(done){ + var store = new session.MemoryStore() + var server = createServer({ store: store, rolling: true, saveUninitialized: true }) - request(app) + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetSessionInStore(store)) + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }); + + it('should force cookie and save modified session even if saveUninitialized option is set to false', function(done){ + var store = new session.MemoryStore() + var server = createServer({ store: store, rolling: true, saveUninitialized: false }, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) + .expect(shouldSetCookie('connect.sid')) + .expect(200, done); + }); + }); + + describe('resave option', function(){ + it('should default to true', function(done){ + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) .expect(200, function(err, res){ if (err) return done(err); - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) - .expect('x-count', '2') + .expect(shouldSetSessionInStore(store)) .expect(200, done); }); }); - it('should pass session touch error', function (done) { - var cb = after(2, done) - var store = new session.MemoryStore() - var server = createServer({ store: store, resave: false }, function (req, res) { - req.session.hit = true - res.end('session saved') + describe('when true', function () { + it('should force save on unmodified session', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, resave: true }, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldSetSessionInStore(store)) + .expect(200, done) + }) + }) + }) + + describe('when false', function () { + it('should prevent save on unmodified session', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, resave: false }, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldNotSetSessionInStore(store)) + .expect(200, done) + }) + }) + + it('should still save modified session', function (done) { + var store = new session.MemoryStore() + var server = createServer({ resave: false, store: store }, function (req, res) { + if (req.method === 'PUT') { + req.session.token = req.url.slice(1) + } + res.end('token=' + (req.session.token || '')) + }) + + request(server) + .put('/w6RHhwaA') + .expect(200) + .expect(shouldSetSessionInStore(store)) + .expect('token=w6RHhwaA') + .end(function (err, res) { + if (err) return done(err) + var sess = cookie(res) + request(server) + .get('/') + .set('Cookie', sess) + .expect(200) + .expect(shouldNotSetSessionInStore(store)) + .expect('token=w6RHhwaA') + .end(function (err) { + if (err) return done(err) + request(server) + .put('/zfQ3rzM3') + .set('Cookie', sess) + .expect(200) + .expect(shouldSetSessionInStore(store)) + .expect('token=zfQ3rzM3') + .end(done) + }) + }) }) - store.touch = function touch(sid, sess, callback) { - callback(new Error('boom!')) - } + it('should detect a "cookie" property as modified', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, resave: false }, function (req, res) { + req.session.user = req.session.user || {} + req.session.user.name = 'bob' + req.session.user.cookie = req.session.user.cookie || 0 + req.session.user.cookie++ + res.end() + }) - server.on('error', function onerror(err) { - assert.ok(err) - assert.equal(err.message, 'boom!') - cb() + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldSetSessionInStore(store)) + .expect(200, done) + }) }) - request(server) - .get('/') - .expect(200, 'session saved', function (err, res) { - if (err) return cb(err) + it('should pass session touch error', function (done) { + var cb = after(2, done) + var store = new session.MemoryStore() + var server = createServer({ store: store, resave: false }, function (req, res) { + req.session.hit = true + res.end('session saved') + }) + + store.touch = function touch (sid, sess, callback) { + callback(new Error('boom!')) + } + + server.on('error', function onerror (err) { + assert.ok(err) + assert.strictEqual(err.message, 'boom!') + cb() + }) + request(server) .get('/') - .set('Cookie', cookie(res)) - .end(cb) + .expect(200, 'session saved', function (err, res) { + if (err) return cb(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .end(cb) + }) }) }) }); describe('saveUninitialized option', function(){ it('should default to true', function(done){ - var count = 0; - var app = express(); - app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + var store = new session.MemoryStore() + var server = createServer({ store: store }) - request(app) + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetSessionInStore(store)) .expect(shouldSetCookie('connect.sid')) .expect(200, done); }); it('should force save of uninitialized session', function(done){ - var count = 0; - var app = express(); - app.use(session({ saveUninitialized: true, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + var store = new session.MemoryStore() + var server = createServer({ store: store, saveUninitialized: true }) - request(app) + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetSessionInStore(store)) .expect(shouldSetCookie('connect.sid')) .expect(200, done); }); it('should prevent save of uninitialized session', function(done){ - var count = 0; - var app = express(); - app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + var store = new session.MemoryStore() + var server = createServer({ store: store, saveUninitialized: false }) - request(app) + request(server) .get('/') - .expect('x-count', '0') + .expect(shouldNotSetSessionInStore(store)) .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, done) }); it('should still save modified session', function(done){ - var count = 0; - var app = express(); - app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }})); - app.use(function(req, res, next){ - var save = req.session.save; - res.setHeader('x-count', count); - req.session.count = count; - req.session.user = 'bob'; - req.session.save = function(fn){ - res.setHeader('x-count', ++count); - return save.call(this, fn); - }; - res.end(); - }); + var store = new session.MemoryStore() + var server = createServer({ store: store, saveUninitialized: false }, function (req, res) { + req.session.count = req.session.count || 0 + req.session.count++ + res.end() + }) - request(app) + request(server) .get('/') - .expect('x-count', '1') + .expect(shouldSetSessionInStore(store)) .expect(shouldSetCookie('connect.sid')) .expect(200, done); }); @@ -1174,7 +1447,7 @@ describe('session()', function(){ server.on('error', function onerror(err) { assert.ok(err) - assert.equal(err.message, 'boom!') + assert.strictEqual(err.message, 'boom!') cb() }) @@ -1205,6 +1478,50 @@ describe('session()', function(){ assert.throws(createServer.bind(null, { secret: [] }), /secret option array/); }) + it('should sign and unsign with a string', function (done) { + var server = createServer({ secret: 'awesome cat' }, function (req, res) { + if (!req.session.user) { + req.session.user = 'bob' + res.end('set') + } else { + res.end('get:' + JSON.stringify(req.session.user)) + } + }) + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'set', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'get:"bob"', done) + }) + }) + + it('should sign and unsign with a Buffer', function (done) { + var server = createServer({ secret: crypto.randomBytes(32) }, function (req, res) { + if (!req.session.user) { + req.session.user = 'bob' + res.end('set') + } else { + res.end('get:' + JSON.stringify(req.session.user)) + } + }) + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'set', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'get:"bob"', done) + }) + }) + describe('when an array', function () { it('should sign cookies', function (done) { var server = createServer({ secret: ['keyboard cat', 'nyan cat'] }, function (req, res) { @@ -1275,30 +1592,28 @@ describe('session()', function(){ it('should default to keep', function(done){ var store = new session.MemoryStore(); - var app = express() - .use(session({ store: store, secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.count = req.session.count || 0; - req.session.count++; - if (req.session.count === 2) req.session = null; - res.end(); - }); + var server = createServer({ store: store }, function (req, res) { + req.session.count = req.session.count || 0 + req.session.count++ + if (req.session.count === 2) req.session = null + res.end() + }) - request(app) + request(server) .get('/') .expect(200, function(err, res){ if (err) return done(err); store.length(function(err, len){ if (err) return done(err); - assert.equal(len, 1) - request(app) + assert.strictEqual(len, 1) + request(server) .get('/') .set('Cookie', cookie(res)) .expect(200, function(err, res){ if (err) return done(err); store.length(function(err, len){ if (err) return done(err); - assert.equal(len, 1) + assert.strictEqual(len, 1) done(); }); }); @@ -1308,30 +1623,28 @@ describe('session()', function(){ it('should allow destroy on req.session = null', function(done){ var store = new session.MemoryStore(); - var app = express() - .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.count = req.session.count || 0; - req.session.count++; - if (req.session.count === 2) req.session = null; - res.end(); - }); + var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { + req.session.count = req.session.count || 0 + req.session.count++ + if (req.session.count === 2) req.session = null + res.end() + }) - request(app) + request(server) .get('/') .expect(200, function(err, res){ if (err) return done(err); store.length(function(err, len){ if (err) return done(err); - assert.equal(len, 1) - request(app) + assert.strictEqual(len, 1) + request(server) .get('/') .set('Cookie', cookie(res)) .expect(200, function(err, res){ if (err) return done(err); store.length(function(err, len){ if (err) return done(err); - assert.equal(len, 0) + assert.strictEqual(len, 0) done(); }); }); @@ -1341,21 +1654,19 @@ describe('session()', function(){ it('should not set cookie if initial session destroyed', function(done){ var store = new session.MemoryStore(); - var app = express() - .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session = null; - res.end(); - }); + var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { + req.session = null + res.end() + }) - request(app) + request(server) .get('/') .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, function(err, res){ if (err) return done(err); store.length(function(err, len){ if (err) return done(err); - assert.equal(len, 0) + assert.strictEqual(len, 0) done(); }); }); @@ -1375,7 +1686,7 @@ describe('session()', function(){ server.on('error', function onerror(err) { assert.ok(err) - assert.equal(err.message, 'boom!') + assert.strictEqual(err.message, 'boom!') cb() }) @@ -1387,36 +1698,69 @@ describe('session()', function(){ describe('res.end patch', function () { it('should correctly handle res.end/res.write patched prior', function (done) { - var app = express() + function setup (req, res) { + utils.writePatch(res) + } - app.use(writePatch()) - app.use(createSession()) - app.use(function (req, res) { + function respond (req, res) { req.session.hit = true res.write('hello, ') res.end('world') - }) + } - request(app) + request(createServer(setup, null, respond)) .get('/') .expect(200, 'hello, world', done) }) it('should correctly handle res.end/res.write patched after', function (done) { - var app = express() - - app.use(createSession()) - app.use(writePatch()) - app.use(function (req, res) { + function respond (req, res) { + utils.writePatch(res) req.session.hit = true res.write('hello, ') res.end('world') - }) + } - request(app) + request(createServer(null, respond)) .get('/') .expect(200, 'hello, world', done) }) + + it('should error when res.end is called twice', function (done) { + var error1 = null + var error2 = null + var server = http.createServer(function (req, res) { + res.end() + + try { + res.setHeader('Content-Length', '3') + res.end('foo') + } catch (e) { + error1 = e + } + }) + + function respond (req, res) { + res.end() + + try { + res.setHeader('Content-Length', '3') + res.end('foo') + } catch (e) { + error2 = e + } + } + + request(server) + .get('/') + .end(function (err, res) { + if (err) return done(err) + request(createServer(null, respond)) + .get('/') + .expect(function () { assert.strictEqual((error1 && error1.message), (error2 && error2.message)) }) + .expect(res.statusCode, res.text, done) + }) + }) }) describe('req.session', function(){ @@ -1445,22 +1789,19 @@ describe('session()', function(){ it('should only set-cookie when modified', function(done){ var modify = true; + var server = createServer(null, function (req, res) { + if (modify) { + req.session.count = req.session.count || 0 + req.session.count++ + } + res.end(req.session.count.toString()) + }) - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) - .use(function(req, res, next){ - if (modify) { - req.session.count = req.session.count || 0; - req.session.count++; - } - res.end(req.session.count.toString()); - }); - - request(app) + request(server) .get('/') .expect(200, '1', function (err, res) { if (err) return done(err) - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) .expect(200, '2', function (err, res) { @@ -1468,7 +1809,7 @@ describe('session()', function(){ var val = cookie(res); modify = false; - request(app) + request(server) .get('/') .set('Cookie', val) .expect(shouldNotHaveHeader('Set-Cookie')) @@ -1476,7 +1817,7 @@ describe('session()', function(){ if (err) return done(err) modify = true; - request(app) + request(server) .get('/') .set('Cookie', val) .expect(shouldSetCookie('connect.sid')) @@ -1487,19 +1828,17 @@ describe('session()', function(){ }) it('should not have enumerable methods', function (done) { - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) - .use(function(req, res, next) { - req.session.foo = 'foo'; - req.session.bar = 'bar'; - var keys = []; - for (var key in req.session) { - keys.push(key); - } - res.end(keys.sort().join(',')); - }); + var server = createServer(null, function (req, res) { + req.session.foo = 'foo' + req.session.bar = 'bar' + var keys = [] + for (var key in req.session) { + keys.push(key) + } + res.end(keys.sort().join(',')) + }) - request(app) + request(server) .get('/') .expect(200, 'bar,cookie,foo', done); }); @@ -1542,51 +1881,41 @@ describe('session()', function(){ describe('.destroy()', function(){ it('should destroy the previous session', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.destroy(function(err){ - if (err) return next(err) - assert(!req.session, 'req.session after destroy'); - res.end(); - }); - }); + var server = createServer(null, function (req, res) { + req.session.destroy(function (err) { + if (err) res.statusCode = 500 + res.end(String(req.session)) + }) + }) - request(app) + request(server) .get('/') .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) + .expect(200, 'undefined', done) }) }) describe('.regenerate()', function(){ it('should destroy/replace the previous session', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) - .use(function(req, res, next){ - var id = req.session.id; - req.session.regenerate(function(err){ - if (err) return next(err) - assert.notEqual(id, req.session.id) - res.end(); - }); - }); + var server = createServer(null, function (req, res) { + var id = req.session.id + req.session.regenerate(function (err) { + if (err) res.statusCode = 500 + res.end(String(req.session.id === id)) + }) + }) - request(app) + request(server) .get('/') .expect(shouldSetCookie('connect.sid')) .expect(200, function (err, res) { if (err) return done(err) - var id = sid(res) - request(app) + request(server) .get('/') .set('Cookie', cookie(res)) .expect(shouldSetCookie('connect.sid')) - .expect(200, function (err, res) { - if (err) return done(err) - assert.notEqual(sid(res), id) - done(); - }); + .expect(shouldSetCookieToDifferentSessionId(sid(res))) + .expect(200, 'false', done) }); }) }) @@ -1660,6 +1989,49 @@ describe('session()', function(){ .expect(500, 'failed to load session', done) }) }) + + it('should not override an overridden `reload` in case of errors', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, resave: false }, function (req, res) { + if (req.url === '/') { + req.session.active = true + res.end('session created') + return + } + + store.clear(function (err) { + if (err) return done(err) + + // reload way too many times on top of each other, + // attempting to overflow the call stack + var iters = 20 + reload() + function reload () { + if (!--iters) { + res.end('ok') + return + } + + try { + req.session.reload(reload) + } catch (e) { + res.statusCode = 500 + res.end(e.message) + } + } + }) + }) + + request(server) + .get('/') + .expect(200, 'session created', function (err, res) { + if (err) return done(err) + request(server) + .get('/foo') + .set('Cookie', cookie(res)) + .expect(200, 'ok', done) + }) + }) }) describe('.save()', function () { @@ -1682,7 +2054,6 @@ describe('session()', function(){ }) it('should prevent end-of-request save', function (done) { - var count = 0 var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true @@ -1692,31 +2063,20 @@ describe('session()', function(){ }) }) - var _set = store.set - store.set = function set(sid, sess, callback) { - count++ - _set.call(store, sid, sess, callback) - } - request(server) .get('/') + .expect(shouldSetSessionInStore(store)) .expect(200, 'saved', function (err, res) { if (err) return done(err) - assert.equal(count, 1) - count = 0 request(server) .get('/') .set('Cookie', cookie(res)) - .expect(200, 'saved', function (err) { - if (err) return done(err) - assert.equal(count, 1) - done() - }) + .expect(shouldSetSessionInStore(store)) + .expect(200, 'saved', done) }) }) it('should prevent end-of-request save on reloaded session', function (done) { - var count = 0 var store = new session.MemoryStore() var server = createServer({ store: store }, function (req, res) { req.session.hit = true @@ -1728,26 +2088,41 @@ describe('session()', function(){ }) }) - var _set = store.set - store.set = function set(sid, sess, callback) { - count++ - _set.call(store, sid, sess, callback) - } - request(server) .get('/') + .expect(shouldSetSessionInStore(store)) .expect(200, 'saved', function (err, res) { if (err) return done(err) - assert.equal(count, 1) - count = 0 request(server) .get('/') .set('Cookie', cookie(res)) - .expect(200, 'saved', function (err) { - if (err) return done(err) - assert.equal(count, 1) - done() + .expect(shouldSetSessionInStore(store)) + .expect(200, 'saved', done) + }) + }) + + describe('when saveUninitialized is false', function () { + it('should prevent end-of-request save', function (done) { + var store = new session.MemoryStore() + var server = createServer({ saveUninitialized: false, store: store }, function (req, res) { + req.session.hit = true + req.session.save(function (err) { + if (err) return res.end(err.message) + res.end('saved') + }) }) + + request(server) + .get('/') + .expect(shouldSetSessionInStore(store)) + .expect(200, 'saved', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldSetSessionInStore(store)) + .expect(200, 'saved', done) + }) }) }) }) @@ -1777,7 +2152,7 @@ describe('session()', function(){ if (err) return done(err); store.get(id, function (err, sess) { if (err) return done(err) - assert.notEqual(new Date(sess.cookie.expires).getTime(), exp.getTime()) + assert.notStrictEqual(new Date(sess.cookie.expires).getTime(), exp.getTime()) done() }) }) @@ -1790,56 +2165,36 @@ describe('session()', function(){ describe('.cookie', function(){ describe('.*', function(){ it('should serialize as parameters', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', proxy: true, cookie: { maxAge: min }})) - .use(function(req, res, next){ - req.session.cookie.httpOnly = false; - req.session.cookie.secure = true; - res.end(); - }); + var server = createServer({ proxy: true }, function (req, res) { + req.session.cookie.httpOnly = false + req.session.cookie.secure = true + res.end() + }) - request(app) + request(server) .get('/') .set('X-Forwarded-Proto', 'https') - .expect(200, function(err, res){ - if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('HttpOnly'), -1, 'should not be HttpOnly cookie') - assert.notEqual(val.indexOf('Secure'), -1, 'should be Secure cookie') - done(); - }); + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'HttpOnly')) + .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure')) + .expect(200, done) }) - - it('should default to a browser-session length cookie', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }})) - .use(function(req, res, next){ - res.end(); - }); - - request(app) - .get('/admin') - .expect(200, function(err, res){ - if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires') - done(); - }); + + it('should default to a browser-session length cookie', function(done){ + request(createServer({ cookie: { path: '/admin' } })) + .get('/admin') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires')) + .expect(200, done) }) it('should Set-Cookie only once for browser-session cookies', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }})) - .use(function(req, res, next){ - res.end(); - }); + var server = createServer({ cookie: { path: '/admin' } }) - request(app) + request(server) .get('/admin/foo') .expect(shouldSetCookie('connect.sid')) .expect(200, function (err, res) { if (err) return done(err) - request(app) + request(server) .get('/admin') .set('Cookie', cookie(res)) .expect(shouldNotHaveHeader('Set-Cookie')) @@ -1848,40 +2203,151 @@ describe('session()', function(){ }) it('should override defaults', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/admin', httpOnly: false, secure: true, maxAge: 5000 }})) - .use(function(req, res, next){ - req.session.cookie.secure = false; - res.end(); - }); + var opts = { + httpOnly: false, + maxAge: 5000, + path: '/admin', + priority: 'high', + secure: true + } + var server = createServer({ cookie: opts }, function (req, res) { + req.session.cookie.secure = false + res.end() + }) - request(app) - .get('/admin') - .expect(200, function(err, res){ - if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('HttpOnly'), -1, 'should not be HttpOnly cookie') - assert.equal(val.indexOf('Secure'), -1, 'should not be Secure cookie') - assert.notEqual(val.indexOf('Path=/admin'), -1, 'should have cookie path /admin') - assert.notEqual(val.indexOf('Expires'), -1, 'should have cookie Expires') - done(); - }); + request(server) + .get('/admin') + .expect(shouldSetCookieWithAttribute('connect.sid', 'Expires')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'HttpOnly')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', '/admin')) + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure')) + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Priority', 'High')) + .expect(200, done) + }) + + it('should forward errors setting cookie', function (done) { + var cb = after(2, done) + var server = createServer({ cookie: { expires: new Date(NaN) } }, function (req, res) { + res.end() + }) + + server.on('error', function onerror (err) { + assert.ok(err) + assert.strictEqual(err.message, 'option expires is invalid') + cb() + }) + + request(server) + .get('/admin') + .expect(200, cb) }) it('should preserve cookies set before writeHead is called', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat' })) - .use(function(req, res, next){ - var cookie = new Cookie(); - res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue')); - res.end(); - }); + var server = createServer(null, function (req, res) { + var cookie = new Cookie() + res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue')) + res.end() + }) - request(app) + request(server) .get('/') .expect(shouldSetCookieToValue('previous', 'cookieValue')) .expect(200, done) }) + + it('should preserve cookies set in writeHead', function (done) { + var server = createServer(null, function (req, res) { + var cookie = new Cookie() + res.writeHead(200, { + 'Set-Cookie': cookie.serialize('previous', 'cookieValue') + }) + res.end() + }) + + request(server) + .get('/') + .expect(shouldSetCookieToValue('previous', 'cookieValue')) + .expect(200, done) + }) + }) + + describe('.originalMaxAge', function () { + it('should equal original maxAge', function (done) { + var server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) { + res.end(JSON.stringify(req.session.cookie.originalMaxAge)) + }) + + request(server) + .get('/') + .expect(200) + .expect(function (res) { + // account for 1ms latency + assert.ok(res.text === '2000' || res.text === '1999', + 'expected 2000, got ' + res.text) + }) + .end(done) + }) + + it('should equal original maxAge for all requests', function (done) { + var server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) { + res.end(JSON.stringify(req.session.cookie.originalMaxAge)) + }) + + request(server) + .get('/') + .expect(200) + .expect(function (res) { + // account for 1ms latency + assert.ok(res.text === '2000' || res.text === '1999', + 'expected 2000, got ' + res.text) + }) + .end(function (err, res) { + if (err) return done(err) + setTimeout(function () { + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200) + .expect(function (res) { + // account for 1ms latency + assert.ok(res.text === '2000' || res.text === '1999', + 'expected 2000, got ' + res.text) + }) + .end(done) + }, 100) + }) + }) + + it('should equal original maxAge for all requests', function (done) { + var store = new SmartStore() + var server = createServer({ cookie: { maxAge: 2000 }, store: store }, function (req, res) { + res.end(JSON.stringify(req.session.cookie.originalMaxAge)) + }) + + request(server) + .get('/') + .expect(200) + .expect(function (res) { + // account for 1ms latency + assert.ok(res.text === '2000' || res.text === '1999', + 'expected 2000, got ' + res.text) + }) + .end(function (err, res) { + if (err) return done(err) + setTimeout(function () { + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200) + .expect(function (res) { + // account for 1ms latency + assert.ok(res.text === '2000' || res.text === '1999', + 'expected 2000, got ' + res.text) + }) + .end(done) + }, 100) + }) + }) }) describe('.secure', function(){ @@ -1924,175 +2390,87 @@ describe('session()', function(){ }) }) - describe('when the pathname does not match cookie.path', function(){ - it('should not set-cookie', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }})) - .use(function(req, res, next){ - if (!req.session) { - return res.end(); - } - req.session.foo = Math.random(); - res.end(); - }); + describe('.maxAge', function () { + before(function (done) { + var ctx = this + + ctx.cookie = '' + ctx.server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) { + switch (++req.session.count) { + case 1: + break + case 2: + req.session.cookie.maxAge = 5000 + break + case 3: + req.session.cookie.maxAge = 3000000000 + break + default: + req.session.count = 0 + break + } + res.end(req.session.count.toString()) + }) - request(app) + request(ctx.server) .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) + .end(function (err, res) { + ctx.cookie = res && cookie(res) + done(err) + }) }) - it('should not set-cookie even for FQDN', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }})) - .use(function(req, res, next){ - if (!req.session) { - return res.end(); - } - - req.session.foo = Math.random(); - res.end(); - }); - - request(app) + it('should set cookie expires relative to maxAge', function (done) { + request(this.server) .get('/') - .set('host', 'http://foo/bar') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }) - }) - - describe('when the pathname does match cookie.path', function(){ - it('should set-cookie', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }})) - .use(function(req, res, next){ - req.session.foo = Math.random(); - res.end(); - }); - - request(app) - .get('/foo/bar/baz') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }) - - it('should set-cookie even for FQDN', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }})) - .use(function(req, res, next){ - req.session.foo = Math.random(); - res.end(); - }); - - request(app) - .get('/foo/bar/baz') - .set('host', 'http://example.com') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) + .set('Cookie', this.cookie) + .expect(shouldSetCookieToExpireIn('connect.sid', 2000)) + .expect(200, '1', done) }) - }) - - describe('.maxAge', function(){ - var val; - var app = express() - .use(session({ secret: 'keyboard cat', cookie: { maxAge: 2000 }})) - .use(function(req, res, next){ - req.session.count = req.session.count || 0; - req.session.count++; - if (req.session.count == 2) req.session.cookie.maxAge = 5000; - if (req.session.count == 3) req.session.cookie.maxAge = 3000000000; - res.end(req.session.count.toString()); - }); - - it('should set relative in milliseconds', function(done){ - request(app) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - var a = new Date(expires(res)) - var b = new Date - var delta = a.valueOf() - b.valueOf() - - val = cookie(res); - - assert.ok(delta > 1000 && delta <= 2000) - done(); - }); - }); - it('should modify cookie when changed', function(done){ - request(app) + it('should modify cookie expires when changed', function (done) { + request(this.server) .get('/') - .set('Cookie', val) - .expect(200, '2', function (err, res) { - if (err) return done(err) - var a = new Date(expires(res)) - var b = new Date - var delta = a.valueOf() - b.valueOf() - - val = cookie(res); - - assert.ok(delta > 4000 && delta <= 5000) - done(); - }); - }); + .set('Cookie', this.cookie) + .expect(shouldSetCookieToExpireIn('connect.sid', 5000)) + .expect(200, '2', done) + }) - it('should modify cookie when changed to large value', function(done){ - request(app) + it('should modify cookie expires when changed to large value', function (done) { + request(this.server) .get('/') - .set('Cookie', val) - .expect(200, '3', function (err, res) { - if (err) return done(err) - var a = new Date(expires(res)) - var b = new Date - var delta = a.valueOf() - b.valueOf() - - val = cookie(res); - - assert.ok(delta > 2999999000 && delta <= 3000000000) - done(); - }); - }); + .set('Cookie', this.cookie) + .expect(shouldSetCookieToExpireIn('connect.sid', 3000000000)) + .expect(200, '3', done) + }) }) describe('.expires', function(){ describe('when given a Date', function(){ it('should set absolute', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.cookie.expires = new Date(0); - res.end(); - }); - - request(app) + var server = createServer(null, function (req, res) { + req.session.cookie.expires = new Date(0) + res.end() + }) + + request(server) .get('/') - .end(function(err, res){ - if (err) return done(err) - assert.equal(expires(res), 'Thu, 01 Jan 1970 00:00:00 GMT') - done(); - }); + .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Expires', 'Thu, 01 Jan 1970 00:00:00 GMT')) + .expect(200, done) }) }) describe('when null', function(){ it('should be a browser-session cookie', function(done){ - var app = express() - .use(session({ secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.cookie.expires = null; - res.end(); - }); - - request(app) + var server = createServer(null, function (req, res) { + req.session.cookie.expires = null + res.end() + }) + + request(server) .get('/') - .expect(200, function(err, res){ - if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires') - done(); - }); + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires')) + .expect(200, done) }) it('should not reset cookie', function (done) { @@ -2103,18 +2481,14 @@ describe('session()', function(){ request(server) .get('/') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires')) .expect(200, function (err, res) { if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires') request(server) .get('/') - .set('Cookie', val) - .expect(200, function (err, res) { - if (err) return done(err); - assert.ok(!cookie(res)); - done(); - }); + .set('Cookie', cookie(res)) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }); }) @@ -2127,22 +2501,53 @@ describe('session()', function(){ request(server) .get('/') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires')) .expect(200, function (err, res) { if (err) return done(err); - var val = cookie(res); - assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires') request(server) .get('/') - .set('Cookie', val) - .expect(200, function (err, res) { - if (err) return done(err); - assert.ok(!cookie(res)); - done(); - }); + .set('Cookie', cookie(res)) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }); }) }) }) + + describe('.partitioned', function () { + describe('by default', function () { + it('should not set partitioned attribute', function (done) { + var server = createServer() + + request(server) + .get('/') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Partitioned')) + .expect(200, done) + }) + }) + + describe('when "false"', function () { + it('should not set partitioned attribute', function (done) { + var server = createServer({ cookie: { partitioned: false } }) + + request(server) + .get('/') + .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Partitioned')) + .expect(200, done) + }) + }) + + describe('when "true"', function () { + it('should set partitioned attribute', function (done) { + var server = createServer({ cookie: { partitioned: true } }) + + request(server) + .get('/') + .expect(shouldSetCookieWithAttribute('connect.sid', 'Partitioned')) + .expect(200, done) + }) + }) + }) }) }) @@ -2189,7 +2594,7 @@ describe('session()', function(){ var app = express() .use(cookieParser()) .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat' })) + .use(createSession()) .use(function(req, res, next){ req.session.count = req.session.count || 0 req.session.count++ @@ -2211,7 +2616,7 @@ describe('session()', function(){ var app = express() .use(cookieParser()) .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat', key: 'sessid' })) + .use(createSession({ key: 'sessid' })) .use(function(req, res, next){ req.session.count = req.session.count || 0 req.session.count++ @@ -2233,7 +2638,7 @@ describe('session()', function(){ var app = express() .use(cookieParser()) .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat', key: 'sessid' })) + .use(createSession({ key: 'sessid' })) .use(function(req, res, next){ req.session.count = req.session.count || 0 req.session.count++ @@ -2256,7 +2661,7 @@ describe('session()', function(){ var app = express() .use(cookieParser('keyboard cat')) .use(function(req, res, next){ delete req.headers.cookie; next() }) - .use(session()) + .use(createSession()) .use(function(req, res, next){ req.session.count = req.session.count || 0 req.session.count++ @@ -2276,13 +2681,134 @@ describe('session()', function(){ }) }) +describe('path matching (RFC 6265)', function () { + describe('when "path" is "/" (root path)', function () { + before(function () { + this.server = createServer({ cookie: { path: '/' } }) + }) + + it('should set cookie when request-path is "/" (root path)', function (done) { + // RFC 6265 5.1.4: "The cookie-path and the request-path are identical." + request(this.server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set cookie when request-path is any path ("/foo")', function (done) { + // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last + // character of the cookie-path is %x2F ("/")." + request(this.server) + .get('/foo') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set cookie when request-path has multiple segments ("/foo/bar/baz")', function (done) { + // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last + // character of the cookie-path is %x2F ("/")." + request(this.server) + .get('/foo/bar/baz') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + }) + + describe('when "path" is "/admin"', function () { + before(function () { + this.server = createServer({ cookie: { path: '/admin' } }) + }) + + it('should set cookie when request-path and cookie-path are identical ("/admin")', function (done) { + // RFC 6265 5.1.4: "The cookie-path and the request-path are identical." + request(this.server) + .get('/admin') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/")', function (done) { + // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last + // character of the cookie-path is %x2F ("/")." + request(this.server) + .get('/admin/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should set cookie when cookie-path is prefix and next char is "/" ("/admin/users")', function (done) { + // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the first + // character of the request-path that is not included in the cookie-path is a %x2F ("/") character." + request(this.server) + .get('/admin/users') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) { + // RFC 6265 5.1.4: None of the path-match conditions are met + request(this.server) + .get('/administrator') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + }) + + describe('when "path" is "/admin/" (trailing slash)', function () { + before(function () { + this.server = createServer({ cookie: { path: '/admin/' } }) + }) + + it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/x")', function (done) { + // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last + // character of the cookie-path is %x2F ("/")." + request(this.server) + .get('/admin/x') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should NOT set cookie when request-path is not prefixed by cookie-path ("/admin")', function (done) { + // RFC 6265 5.1.4: cookie-path "/admin/" is not a prefix of request-path "/admin" + request(this.server) + .get('/admin') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + + it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) { + // RFC 6265 5.1.4: None of the path-match conditions are met: + // 1. The paths are not identical + // 2. "/admin/" is not a prefix of "/administrator" + // 3. The prefix condition with next character "/" is not applicable + request(this.server) + .get('/administrator') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + }) +}) + + function cookie(res) { var setCookie = res.headers['set-cookie']; return (setCookie && setCookie[0]) || undefined; } -function createServer(opts, fn) { - return http.createServer(createRequestListener(opts, fn)) +function createServer (options, respond) { + var fn = respond + var opts = options + var server = http.createServer() + + // setup, options, respond + if (typeof arguments[0] === 'function') { + opts = arguments[1] + fn = arguments[2] + + server.on('request', arguments[0]) + } + + return server.on('request', createRequestListener(opts, fn)) } function createRequestListener(opts, fn) { @@ -2327,9 +2853,18 @@ function end(req, res) { res.end() } -function expires(res) { - var match = /Expires=([^;]+)/.exec(cookie(res)); - return match ? match[1] : undefined; +function expires (res) { + var header = cookie(res) + return header && utils.parseSetCookie(header).expires +} + +function mountAt (path) { + return function (req, res) { + if (req.url.indexOf(path) === 0) { + req.originalUrl = req.url + req.url = req.url.slice(path.length) + } + } } function shouldNotHaveHeader(header) { @@ -2338,87 +2873,118 @@ function shouldNotHaveHeader(header) { } } -function shouldNotSetSecureCookie(name) { +function shouldNotSetSessionInStore(store) { + var _set = store.set + var count = 0 + + store.set = function set () { + count++ + return _set.apply(this, arguments) + } + + return function () { + assert.ok(count === 0, 'should not set session in store') + } +} + +function shouldSetCookie (name) { + return function (res) { + var header = cookie(res) + var data = header && utils.parseSetCookie(header) + assert.ok(header, 'should have a cookie header') + assert.strictEqual(data.name, name, 'should set cookie ' + name) + } +} + +function shouldSetCookieToDifferentSessionId (id) { + return function (res) { + assert.notStrictEqual(sid(res), id) + } +} + +function shouldSetCookieToExpireIn (name, delta) { return function (res) { var header = cookie(res) + var data = header && utils.parseSetCookie(header) assert.ok(header, 'should have a cookie header') - assert.equal(header.split('=')[0], name, 'should set cookie ' + name) - assert.ok(header.toLowerCase().split(/; */).every(function (k) { return k !== 'secure'; }), 'should not set secure cookie') + assert.strictEqual(data.name, name, 'should set cookie ' + name) + assert.ok(('expires' in data), 'should set cookie with attribute Expires') + assert.ok(('date' in res.headers), 'should have a date header') + assert.strictEqual((Date.parse(data.expires) - Date.parse(res.headers.date)), delta, 'should set cookie ' + name + ' to expire in ' + delta + ' ms') } } -function shouldSetCookie(name) { +function shouldSetCookieToValue (name, val) { return function (res) { var header = cookie(res) + var data = header && utils.parseSetCookie(header) assert.ok(header, 'should have a cookie header') - assert.equal(header.split('=')[0], name, 'should set cookie ' + name) + assert.strictEqual(data.name, name, 'should set cookie ' + name) + assert.strictEqual(data.value, val, 'should set cookie ' + name + ' to ' + val) } } -function shouldSetCookieToValue(name, val) { +function shouldSetCookieWithAttribute (name, attrib) { return function (res) { - var header = cookie(res); + var header = cookie(res) + var data = header && utils.parseSetCookie(header) assert.ok(header, 'should have a cookie header') - assert.equal(header.split('=')[0], name, 'should set cookie ' + name) - assert.equal(header.split('=')[1].split(';')[0], val, 'should set cookie ' + name + ' to ' + val) + assert.strictEqual(data.name, name, 'should set cookie ' + name) + assert.ok((attrib.toLowerCase() in data), 'should set cookie with attribute ' + attrib) } } -function shouldSetSecureCookie(name) { +function shouldSetCookieWithAttributeAndValue (name, attrib, value) { return function (res) { var header = cookie(res) + var data = header && utils.parseSetCookie(header) assert.ok(header, 'should have a cookie header') - assert.equal(header.split('=')[0], name, 'should set cookie ' + name) - assert.ok(header.toLowerCase().split(/; */).some(function (k) { return k === 'secure'; }), 'should set secure cookie') + assert.strictEqual(data.name, name, 'should set cookie ' + name) + assert.ok((attrib.toLowerCase() in data), 'should set cookie with attribute ' + attrib) + assert.strictEqual(data[attrib.toLowerCase()], value, 'should set cookie with attribute ' + attrib + ' set to ' + value) } } -function sid(res) { - var match = /^[^=]+=s%3A([^;\.]+)[\.;]/.exec(cookie(res)) - var val = match ? match[1] : undefined - return val +function shouldSetCookieWithoutAttribute (name, attrib) { + return function (res) { + var header = cookie(res) + var data = header && utils.parseSetCookie(header) + assert.ok(header, 'should have a cookie header') + assert.strictEqual(data.name, name, 'should set cookie ' + name) + assert.ok(!(attrib.toLowerCase() in data), 'should set cookie without attribute ' + attrib) + } } -function writePatch() { - var ended = false - return function addWritePatch(req, res, next) { - var _end = res.end - var _write = res.write +function shouldSetSessionInStore (store, delay) { + var _set = store.set + var count = 0 - res.end = function end() { - ended = true - return _end.apply(this, arguments) + store.set = function set () { + count++ + + if (!delay) { + return _set.apply(this, arguments) } - res.write = function write() { - if (ended) { - throw new Error('write after end') - } + var args = new Array(arguments.length + 1) - return _write.apply(this, arguments) + args[0] = this + for (var i = 1; i < args.length; i++) { + args[i] = arguments[i - 1] } - next() + setTimeout(_set.bind.apply(_set, args), delay) } -} -function SyncStore () { - session.Store.call(this) - this.sessions = Object.create(null) + return function () { + assert.ok(count === 1, 'should set session in store') + } } -util.inherits(SyncStore, session.Store) - -SyncStore.prototype.destroy = function destroy(sid, callback) { - delete this.sessions[sid]; - callback(); -}; - -SyncStore.prototype.get = function get(sid, callback) { - callback(null, JSON.parse(this.sessions[sid])); -}; - -SyncStore.prototype.set = function set(sid, sess, callback) { - this.sessions[sid] = JSON.stringify(sess); - callback(); -}; +function sid (res) { + var header = cookie(res) + var data = header && utils.parseSetCookie(header) + var value = data && unescape(data.value) + var sid = value && value.substring(2, value.indexOf('.')) + return sid || undefined +} diff --git a/test/support/env.js b/test/support/env.js new file mode 100644 index 00000000..fba1bd89 --- /dev/null +++ b/test/support/env.js @@ -0,0 +1,2 @@ + +process.env.NO_DEPRECATION = 'express-session'; diff --git a/test/support/gencert.sh b/test/support/gencert.sh new file mode 100755 index 00000000..cfdda070 --- /dev/null +++ b/test/support/gencert.sh @@ -0,0 +1,5 @@ +#! /bin/sh +set -ex + +openssl req -x509 -nodes -newkey rsa:2048 -keyout ./test/fixtures/server.key -out ./test/fixtures/server.crt -days 3650 \ +-subj "/C=US/ST=Illinois/L=Chicago/O=node-express-session/CN=express-session.local" diff --git a/test/support/smart-store.js b/test/support/smart-store.js new file mode 100644 index 00000000..8b224fdd --- /dev/null +++ b/test/support/smart-store.js @@ -0,0 +1,54 @@ +'use strict' + +var session = require('../../') +var util = require('util') + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +module.exports = SmartStore + +function SmartStore () { + session.Store.call(this) + this.sessions = Object.create(null) +} + +util.inherits(SmartStore, session.Store) + +SmartStore.prototype.destroy = function destroy (sid, callback) { + delete this.sessions[sid] + defer(callback, null) +} + +SmartStore.prototype.get = function get (sid, callback) { + var sess = this.sessions[sid] + + if (!sess) { + return + } + + // parse + sess = JSON.parse(sess) + + if (sess.cookie) { + // expand expires into Date object + sess.cookie.expires = typeof sess.cookie.expires === 'string' + ? new Date(sess.cookie.expires) + : sess.cookie.expires + + // destroy expired session + if (sess.cookie.expires && sess.cookie.expires <= Date.now()) { + delete this.sessions[sid] + sess = null + } + } + + defer(callback, null, sess) +} + +SmartStore.prototype.set = function set (sid, sess, callback) { + this.sessions[sid] = JSON.stringify(sess) + defer(callback, null) +} diff --git a/test/support/sync-store.js b/test/support/sync-store.js new file mode 100644 index 00000000..b45dc9a8 --- /dev/null +++ b/test/support/sync-store.js @@ -0,0 +1,27 @@ +'use strict' + +var session = require('../../') +var util = require('util') + +module.exports = SyncStore + +function SyncStore () { + session.Store.call(this) + this.sessions = Object.create(null) +} + +util.inherits(SyncStore, session.Store) + +SyncStore.prototype.destroy = function destroy (sid, callback) { + delete this.sessions[sid] + callback() +} + +SyncStore.prototype.get = function get (sid, callback) { + callback(null, JSON.parse(this.sessions[sid])) +} + +SyncStore.prototype.set = function set (sid, sess, callback) { + this.sessions[sid] = JSON.stringify(sess) + callback() +} diff --git a/test/support/utils.js b/test/support/utils.js new file mode 100644 index 00000000..0f13ba6a --- /dev/null +++ b/test/support/utils.js @@ -0,0 +1,42 @@ +'use strict' + +module.exports.parseSetCookie = parseSetCookie +module.exports.writePatch = writePatch + +function parseSetCookie (header) { + var match + var pairs = [] + var pattern = /\s*([^=;]+)(?:=([^;]*);?|;|$)/g + + while ((match = pattern.exec(header))) { + pairs.push({ name: match[1], value: match[2] }) + } + + var cookie = pairs.shift() + + for (var i = 0; i < pairs.length; i++) { + match = pairs[i] + cookie[match.name.toLowerCase()] = (match.value || true) + } + + return cookie +} + +function writePatch (res) { + var _end = res.end + var _write = res.write + var ended = false + + res.end = function end () { + ended = true + return _end.apply(this, arguments) + } + + res.write = function write () { + if (ended) { + throw new Error('write after end') + } + + return _write.apply(this, arguments) + } +}