diff --git a/.gitignore b/.gitignore index df9af16b..b2adb1ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -coverage -node_modules -npm-debug.log +coverage +node_modules +npm-debug.log +/nbproject/private/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 86391141..8b20b501 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,20 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.12" - - "1.0" - - "1.8" - - "2.0" - - "2.5" - - "3.0" -sudo: false -before_install: - # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" -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" +language: node_js +node_js: + - "0.8" + - "0.10" + - "0.12" + - "1.0" + - "1.8" + - "2.0" + - "2.5" + - "3.0" +sudo: false +before_install: + # Setup Node.js version-specific dependencies + - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" +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 c0842e09..f4f42c65 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,268 +1,268 @@ -1.11.3 / 2015-05-22 -=================== - - * deps: cookie@0.1.3 - - Slight optimizations - * deps: crc@3.3.0 - -1.11.2 / 2015-05-10 -=================== - - * deps: debug@~2.2.0 - - deps: ms@0.7.1 - * deps: uid-safe@~2.0.0 - -1.11.1 / 2015-04-08 -=================== - - * Fix mutating `options.secret` value - -1.11.0 / 2015-04-07 -=================== - - * Support an array in `secret` option for key rotation - * deps: depd@~1.0.1 - -1.10.4 / 2015-03-15 -=================== - - * deps: debug@~2.1.3 - - Fix high intensity foreground color for bold - - deps: ms@0.7.0 - -1.10.3 / 2015-02-16 -=================== - - * deps: cookie-signature@1.0.6 - * deps: uid-safe@1.1.0 - - Use `crypto.randomBytes`, if available - - deps: base64-url@1.2.1 - -1.10.2 / 2015-01-31 -=================== - - * deps: uid-safe@1.0.3 - - Fix error branch that would throw - - deps: base64-url@1.2.0 - -1.10.1 / 2015-01-08 -=================== - - * deps: uid-safe@1.0.2 - - Remove dependency on `mz` - -1.10.0 / 2015-01-05 -=================== - - * Add `store.touch` interface for session stores - * Fix `MemoryStore` expiration with `resave: false` - * deps: debug@~2.1.1 - -1.9.3 / 2014-12-02 -================== - - * Fix error when `req.sessionID` contains a non-string value - -1.9.2 / 2014-11-22 -================== - - * deps: crc@3.2.1 - - Minor fixes - -1.9.1 / 2014-10-22 -================== - - * Remove unnecessary empty write call - - Fixes Node.js 0.11.14 behavior change - - Helps work-around Node.js 0.10.1 zlib bug - -1.9.0 / 2014-09-16 -================== - - * deps: debug@~2.1.0 - - Implement `DEBUG_FD` env variable support - * deps: depd@~1.0.0 - -1.8.2 / 2014-09-15 -================== - - * Use `crc` instead of `buffer-crc32` for speed - * deps: depd@0.4.5 - -1.8.1 / 2014-09-08 -================== - - * Keep `req.session.save` non-enumerable - * Prevent session prototype methods from being overwritten - -1.8.0 / 2014-09-07 -================== - - * Do not resave already-saved session at end of request - * deps: cookie-signature@1.0.5 - * deps: debug@~2.0.0 - -1.7.6 / 2014-08-18 -================== - - * Fix exception on `res.end(null)` calls - -1.7.5 / 2014-08-10 -================== - - * Fix parsing original URL - * deps: on-headers@~1.0.0 - * deps: parseurl@~1.3.0 - -1.7.4 / 2014-08-05 -================== - - * Fix response end delay for non-chunked responses - -1.7.3 / 2014-08-05 -================== - - * Fix `res.end` patch to call correct upstream `res.write` - -1.7.2 / 2014-07-27 -================== - - * deps: depd@0.4.4 - - Work-around v8 generating empty stack traces - -1.7.1 / 2014-07-26 -================== - - * deps: depd@0.4.3 - - Fix exception when global `Error.stackTraceLimit` is too low - -1.7.0 / 2014-07-22 -================== - - * Improve session-ending error handling - - Errors are passed to `next(err)` instead of `console.error` - * deps: debug@1.0.4 - * deps: depd@0.4.2 - - Add `TRACE_DEPRECATION` environment variable - - Remove non-standard grey color from color output - - Support `--no-deprecation` argument - - Support `--trace-deprecation` argument - -1.6.5 / 2014-07-11 -================== - - * Do not require `req.originalUrl` - * deps: debug@1.0.3 - - Add support for multiple wildcards in namespaces - -1.6.4 / 2014-07-07 -================== - - * Fix blank responses for stores with synchronous operations - -1.6.3 / 2014-07-04 -================== - - * Fix resave deprecation message - -1.6.2 / 2014-07-04 -================== - - * Fix confusing option deprecation messages - -1.6.1 / 2014-06-28 -================== - - * Fix saveUninitialized deprecation message - -1.6.0 / 2014-06-28 -================== - - * Add deprecation message to undefined `resave` option - * Add deprecation message to undefined `saveUninitialized` option - * Fix `res.end` patch to return correct value - * Fix `res.end` patch to handle multiple `res.end` calls - * Reject cookies with missing signatures - -1.5.2 / 2014-06-26 -================== - - * deps: cookie-signature@1.0.4 - - fix for timing attacks - -1.5.1 / 2014-06-21 -================== - - * Move hard-to-track-down `req.secret` deprecation message - -1.5.0 / 2014-06-19 -================== - - * Debug name is now "express-session" - * Deprecate integration with `cookie-parser` middleware - * Deprecate looking for secret in `req.secret` - * Directly read cookies; `cookie-parser` no longer required - * Directly set cookies; `res.cookie` no longer required - * Generate session IDs with `uid-safe`, faster and even less collisions - -1.4.0 / 2014-06-17 -================== - - * Add `genid` option to generate custom session IDs - * Add `saveUninitialized` option to control saving uninitialized sessions - * Add `unset` option to control unsetting `req.session` - * Generate session IDs with `rand-token` by default; reduce collisions - * deps: buffer-crc32@0.2.3 - -1.3.1 / 2014-06-14 -================== - - * Add description in package for npmjs.org listing - -1.3.0 / 2014-06-14 -================== - - * Integrate with express "trust proxy" by default - * deps: debug@1.0.2 - -1.2.1 / 2014-05-27 -================== - - * Fix `resave` such that `resave: true` works - -1.2.0 / 2014-05-19 -================== - - * Add `resave` option to control saving unmodified sessions - -1.1.0 / 2014-05-12 -================== - - * Add `name` option; replacement for `key` option - * Use `setImmediate` in MemoryStore for node.js >= 0.10 - -1.0.4 / 2014-04-27 -================== - - * deps: debug@0.8.1 - -1.0.3 / 2014-04-19 -================== - - * Use `res.cookie()` instead of `res.setHeader()` - * deps: cookie@0.1.2 - -1.0.2 / 2014-02-23 -================== - - * Add missing dependency to `package.json` - -1.0.1 / 2014-02-15 -================== - - * Add missing dependencies to `package.json` - -1.0.0 / 2014-02-15 -================== - - * Genesis from `connect` +1.11.3 / 2015-05-22 +=================== + + * deps: cookie@0.1.3 + - Slight optimizations + * deps: crc@3.3.0 + +1.11.2 / 2015-05-10 +=================== + + * deps: debug@~2.2.0 + - deps: ms@0.7.1 + * deps: uid-safe@~2.0.0 + +1.11.1 / 2015-04-08 +=================== + + * Fix mutating `options.secret` value + +1.11.0 / 2015-04-07 +=================== + + * Support an array in `secret` option for key rotation + * deps: depd@~1.0.1 + +1.10.4 / 2015-03-15 +=================== + + * deps: debug@~2.1.3 + - Fix high intensity foreground color for bold + - deps: ms@0.7.0 + +1.10.3 / 2015-02-16 +=================== + + * deps: cookie-signature@1.0.6 + * deps: uid-safe@1.1.0 + - Use `crypto.randomBytes`, if available + - deps: base64-url@1.2.1 + +1.10.2 / 2015-01-31 +=================== + + * deps: uid-safe@1.0.3 + - Fix error branch that would throw + - deps: base64-url@1.2.0 + +1.10.1 / 2015-01-08 +=================== + + * deps: uid-safe@1.0.2 + - Remove dependency on `mz` + +1.10.0 / 2015-01-05 +=================== + + * Add `store.touch` interface for session stores + * Fix `MemoryStore` expiration with `resave: false` + * deps: debug@~2.1.1 + +1.9.3 / 2014-12-02 +================== + + * Fix error when `req.sessionID` contains a non-string value + +1.9.2 / 2014-11-22 +================== + + * deps: crc@3.2.1 + - Minor fixes + +1.9.1 / 2014-10-22 +================== + + * Remove unnecessary empty write call + - Fixes Node.js 0.11.14 behavior change + - Helps work-around Node.js 0.10.1 zlib bug + +1.9.0 / 2014-09-16 +================== + + * deps: debug@~2.1.0 + - Implement `DEBUG_FD` env variable support + * deps: depd@~1.0.0 + +1.8.2 / 2014-09-15 +================== + + * Use `crc` instead of `buffer-crc32` for speed + * deps: depd@0.4.5 + +1.8.1 / 2014-09-08 +================== + + * Keep `req.session.save` non-enumerable + * Prevent session prototype methods from being overwritten + +1.8.0 / 2014-09-07 +================== + + * Do not resave already-saved session at end of request + * deps: cookie-signature@1.0.5 + * deps: debug@~2.0.0 + +1.7.6 / 2014-08-18 +================== + + * Fix exception on `res.end(null)` calls + +1.7.5 / 2014-08-10 +================== + + * Fix parsing original URL + * deps: on-headers@~1.0.0 + * deps: parseurl@~1.3.0 + +1.7.4 / 2014-08-05 +================== + + * Fix response end delay for non-chunked responses + +1.7.3 / 2014-08-05 +================== + + * Fix `res.end` patch to call correct upstream `res.write` + +1.7.2 / 2014-07-27 +================== + + * deps: depd@0.4.4 + - Work-around v8 generating empty stack traces + +1.7.1 / 2014-07-26 +================== + + * deps: depd@0.4.3 + - Fix exception when global `Error.stackTraceLimit` is too low + +1.7.0 / 2014-07-22 +================== + + * Improve session-ending error handling + - Errors are passed to `next(err)` instead of `console.error` + * deps: debug@1.0.4 + * deps: depd@0.4.2 + - Add `TRACE_DEPRECATION` environment variable + - Remove non-standard grey color from color output + - Support `--no-deprecation` argument + - Support `--trace-deprecation` argument + +1.6.5 / 2014-07-11 +================== + + * Do not require `req.originalUrl` + * deps: debug@1.0.3 + - Add support for multiple wildcards in namespaces + +1.6.4 / 2014-07-07 +================== + + * Fix blank responses for stores with synchronous operations + +1.6.3 / 2014-07-04 +================== + + * Fix resave deprecation message + +1.6.2 / 2014-07-04 +================== + + * Fix confusing option deprecation messages + +1.6.1 / 2014-06-28 +================== + + * Fix saveUninitialized deprecation message + +1.6.0 / 2014-06-28 +================== + + * Add deprecation message to undefined `resave` option + * Add deprecation message to undefined `saveUninitialized` option + * Fix `res.end` patch to return correct value + * Fix `res.end` patch to handle multiple `res.end` calls + * Reject cookies with missing signatures + +1.5.2 / 2014-06-26 +================== + + * deps: cookie-signature@1.0.4 + - fix for timing attacks + +1.5.1 / 2014-06-21 +================== + + * Move hard-to-track-down `req.secret` deprecation message + +1.5.0 / 2014-06-19 +================== + + * Debug name is now "express-session" + * Deprecate integration with `cookie-parser` middleware + * Deprecate looking for secret in `req.secret` + * Directly read cookies; `cookie-parser` no longer required + * Directly set cookies; `res.cookie` no longer required + * Generate session IDs with `uid-safe`, faster and even less collisions + +1.4.0 / 2014-06-17 +================== + + * Add `genid` option to generate custom session IDs + * Add `saveUninitialized` option to control saving uninitialized sessions + * Add `unset` option to control unsetting `req.session` + * Generate session IDs with `rand-token` by default; reduce collisions + * deps: buffer-crc32@0.2.3 + +1.3.1 / 2014-06-14 +================== + + * Add description in package for npmjs.org listing + +1.3.0 / 2014-06-14 +================== + + * Integrate with express "trust proxy" by default + * deps: debug@1.0.2 + +1.2.1 / 2014-05-27 +================== + + * Fix `resave` such that `resave: true` works + +1.2.0 / 2014-05-19 +================== + + * Add `resave` option to control saving unmodified sessions + +1.1.0 / 2014-05-12 +================== + + * Add `name` option; replacement for `key` option + * Use `setImmediate` in MemoryStore for node.js >= 0.10 + +1.0.4 / 2014-04-27 +================== + + * deps: debug@0.8.1 + +1.0.3 / 2014-04-19 +================== + + * Use `res.cookie()` instead of `res.setHeader()` + * deps: cookie@0.1.2 + +1.0.2 / 2014-02-23 +================== + + * Add missing dependency to `package.json` + +1.0.1 / 2014-02-15 +================== + + * Add missing dependencies to `package.json` + +1.0.0 / 2014-02-15 +================== + + * Genesis from `connect` diff --git a/LICENSE b/LICENSE index 9b59ff85..eaf11a1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,24 @@ -(The MIT License) - -Copyright (c) 2010 Sencha Inc. -Copyright (c) 2011 TJ Holowaychuk -Copyright (c) 2014-2015 Douglas Christopher Wilson - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index abcd337b..2b073ca3 100644 --- a/README.md +++ b/README.md @@ -1,500 +1,4 @@ -# express-session - -[![NPM Version][npm-image]][npm-url] -[![NPM Downloads][downloads-image]][downloads-url] -[![Build Status][travis-image]][travis-url] -[![Test Coverage][coveralls-image]][coveralls-url] -[![Gratipay][gratipay-image]][gratipay-url] - -## Installation - -```bash -$ npm install express-session -``` - -## API - -```js -var session = require('express-session') -``` - -### session(options) - -Create a session middleware with the given `options`. - -**Note** Session data is _not_ saved in the cookie itself, just the session ID. -Session data is stored server-side. - -**Warning** The default server-side session storage, `MemoryStore`, is _purposely_ -not designed for a production environment. It will leak memory under most -conditions, does not scale past a single process, and is meant for debugging and -developing. - -For a list of stores, see [compatible session stores](#compatible-session-stores). - -#### Options - -`express-session` accepts these properties in the options object. - -##### cookie - -Settings for the session ID cookie. See the "Cookie options" section below for -more information on the different values. - -The default value is `{ path: '/', httpOnly: true, secure: false, maxAge: null }`. - -##### genid - -Function to call to generate a new session ID. Provide a function that returns -a string that will be used as a session ID. The function is given `req` as the -first argument if you want to use some value attached to `req` when generating -the ID. - -The default value is a function which uses the `uid2` library to generate IDs. - -**NOTE** be careful to generate unique IDs so your sessions do not conflict. - -```js -app.use(session({ - genid: function(req) { - return genuuid() // use UUIDs for session IDs - }, - secret: 'keyboard cat' -})) -``` - -##### name - -The name of the session ID cookie to set in the response (and read from in the -request). - -The default value is `'connect.sid'`. - -**Note** if you have multiple apps running on the same host (hostname + port), -then you need to separate the session cookies from each other. The simplest -method is to simply set different `name`s per app. - -##### proxy - -Trust the reverse proxy when setting secure cookies (via the "X-Forwarded-Proto" -header). - -The default value is `undefined`. - - - `true` The "X-Forwarded-Proto" header will be used. - - `false` All headers are ignored and the connection is considered secure only - if there is a direct TLS/SSL connection. - - `undefined` Uses the "trust proxy" setting from express - -##### resave - -Forces the session to be saved back to the session store, even if the session -was never modified during the request. Depending on your store this may be -necessary, but it can also create race conditions where a client makes two -parallel requests to your server and changes made to the session in one -request may get overwritten when the other request ends, even if it made no -changes (this behavior also depends on what store you're using). - -The default value is `true`, but using the default has been deprecated, -as the default will change in the future. Please research into this setting -and choose what is appropriate to your use-case. Typically, you'll want -`false`. - -How do I know if this is necessary for my store? The best way to know is to -check with your store if it implements the `touch` method. If it does, then -you can safely set `resave: false`. If it does not implement the `touch` -method and your store sets an expiration date on stored sessions, then you -likely need `resave: true`. - -##### rolling - -Force a cookie to be set on every response. This resets the expiration date. - -The default value is `false`. - -##### saveUninitialized - -Forces a session that is "uninitialized" to be saved to the store. A session is -uninitialized when it is new but not modified. Choosing `false` is useful for -implementing login sessions, reducing server storage usage, or complying with -laws that require permission before setting a cookie. Choosing `false` will also -help with race conditions where a client makes multiple parallel requests -without a session. - -The default value is `true`, but using the default has been deprecated, as the -default will change in the future. Please research into this setting and -choose what is appropriate to your use-case. - -**Note** if you are using Session in conjunction with PassportJS, Passport -will add an empty Passport object to the session for use after a user is -authenticated, which will be treated as a modification to the session, causing -it to be saved. - -##### secret - -**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. - -##### store - -The session store instance, defaults to a new `MemoryStore` instance. - -##### unset - -Control the result of unsetting `req.session` (through `delete`, setting to `null`, -etc.). - -The default value is `'keep'`. - - - `'destroy'` The session will be destroyed (deleted) when the response ends. - - `'keep'` The session in the store will be kept, but modifications made during - the request are ignored and not saved. - -#### Cookie options - -**Note** Since version 1.5.0, the [`cookie-parser` middleware](https://www.npmjs.com/package/cookie-parser) -no longer needs to be used for this module to work. This module now directly reads -and writes cookies on `req`/`res`. Using `cookie-parser` may result in issues -if the `secret` is not the same between this module and `cookie-parser`. - -Please note that `secure: true` is a **recommended** option. However, it requires an https-enabled website, i.e., HTTPS is necessary for secure cookies. -If `secure` is set, and you access your site over HTTP, the cookie will not be set. If you have your node.js behind a proxy and are using `secure: true`, you need to set "trust proxy" in express: - -```js -var app = express() -app.set('trust proxy', 1) // trust first proxy -app.use(session({ - secret: 'keyboard cat', - resave: false, - saveUninitialized: true, - cookie: { secure: true } -})) -``` - -For using secure cookies in production, but allowing for testing in development, the following is an example of enabling this setup based on `NODE_ENV` in express: - -```js -var app = express() -var sess = { - secret: 'keyboard cat', - cookie: {} -} - -if (app.get('env') === 'production') { - app.set('trust proxy', 1) // trust first proxy - sess.cookie.secure = true // serve secure cookies -} - -app.use(session(sess)) -``` - -By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set -so the cookie becomes a browser-session cookie. When the user closes the -browser the cookie (and session) will be removed. - -### req.session - -To store or access session data, simply use the request property `req.session`, -which is (generally) serialized as JSON by the store, so nested objects -are typically fine. For example below is a user-specific view counter: - -```js -app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) - -app.use(function(req, res, next) { - var sess = req.session - if (sess.views) { - sess.views++ - res.setHeader('Content-Type', 'text/html') - res.write('

views: ' + sess.views + '

') - res.write('

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

') - res.end() - } else { - sess.views = 1 - res.end('welcome to the session demo. refresh!') - } -}) -``` - -#### Session.regenerate() - -To regenerate the session simply invoke the method. Once complete, -a new SID and `Session` instance will be initialized at `req.session`. - -```js -req.session.regenerate(function(err) { - // will have a new session here -}) -``` - -#### Session.destroy() - -Destroys the session, removing `req.session`; will be re-generated next request. - -```js -req.session.destroy(function(err) { - // cannot access session here -}) -``` - -#### Session.reload() - -Reloads the session data. - -```js -req.session.reload(function(err) { - // session updated -}) -``` - -#### Session.save() - -```js -req.session.save(function(err) { - // session saved -}) -``` - -#### Session.touch() - -Updates the `.maxAge` property. Typically this is -not necessary to call, as the session middleware does this for you. - -### req.session.cookie - -Each session has a unique cookie object accompany it. This allows -you to alter the session cookie per visitor. For example we can -set `req.session.cookie.expires` to `false` to enable the cookie -to remain for only the duration of the user-agent. - -#### Cookie.maxAge - -Alternatively `req.session.cookie.maxAge` will return the time -remaining in milliseconds, which we may also re-assign a new value -to adjust the `.expires` property appropriately. The following -are essentially equivalent - -```js -var hour = 3600000 -req.session.cookie.expires = new Date(Date.now() + hour) -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. - -```js -req.session.cookie.maxAge // => 30000 -``` - -### req.sessionID - -To get the ID of the loaded session, access the request property -`req.sessionID`. This is simply a read-only value set when a session -is loaded/created. - -## Session Store Implementation - -Every session store _must_ be an `EventEmitter` and implement specific -methods. The following methods are the list of **required**, **recommended**, -and **optional**. - - * Required methods are ones that this module will always call on the store. - * Recommended methods are ones that this module will call on the store if - available. - * Optional methods are ones this module does not call at all, but helps - present uniform stores to users. - -For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. - -### store.all(callback) - -**Optional** - -This optional method is used to get all sessions in the store as an array. The -`callback` should be called as `callback(error, sessions)`. - -### store.destroy(sid, callback) - -**Required** - -This required method is used to destroy/delete a session from the store given -a session ID (`sid`). The `callback` should be called as `callback(error)` once -the session is destroyed. - -### store.clear(callback) - -**Optional** - -This optional method is used to delete all sessions from the store. The -`callback` should be called as `callback(error)` once the store is cleared. - -### store.length(callback) - -**Optional** - -This optional method is used to get the count of all sessions in the store. -The `callback` should be called as `callback(error, len)`. - -### store.get(sid, callback) - -**Required** - -This required method is used to get a session from the store given a session -ID (`sid`). The `callback` should be called as `callback(error, session)`. - -The `session` argument should be a session if found, otherwise `null` or -`undefined` if the session was not found (and there was no error). A special -case is made when `error.code === 'ENOENT'` to act like `callback(null, null)`. - -### store.set(sid, session, callback) - -**Required** - -This required method is used to upsert a session into the store given a -session ID (`sid`) and session (`session`) object. The callback should be -called as `callback(error)` once the session has been set in the store. - -### store.touch(sid, session, callback) - -**Recommended** - -This recommended method is used to "touch" a given session given a -session ID (`sid`) and session (`session`) object. The `callback` should be -called as `callback(error)` once the session has been touched. - -This is primarily used when the store will automatically delete idle sessions -and this method is used to signal to the store the given session is active, -potentially resetting the idle timer. - -## Compatible Session Stores - -The following modules implement a session store that is compatible with this -module. Please make a PR to add additional modules :) - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][connect-mssql-image] connect-mssql][connect-mssql-url] A SQL Server-based session store. -[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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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. -[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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][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 - -[![Github Stars][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store. -[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 - -## Example - -A simple example using `express-session` to store page views for a user. - -```js -var express = require('express') -var parseurl = require('parseurl') -var session = require('express-session') - -var app = express() - -app.use(session({ - secret: 'keyboard cat', - resave: false, - saveUninitialized: true -})) - -app.use(function (req, res, next) { - var views = req.session.views - - if (!views) { - views = req.session.views = {} - } - - // get the url pathname - var pathname = parseurl(req).pathname - - // count the views - views[pathname] = (views[pathname] || 0) + 1 - - next() -}) - -app.get('/foo', function (req, res, next) { - res.send('you viewed this page ' + req.session.views['/foo'] + ' times') -}) - -app.get('/bar', function (req, res, next) { - res.send('you viewed this page ' + req.session.views['/bar'] + ' times') -}) -``` - -## 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 -[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/ +-------- Modification Date ------------ + + +-----------2015.4.9------------ \ No newline at end of file diff --git a/index.js b/index.js index 5e12c9fc..34f07582 100644 --- a/index.js +++ b/index.js @@ -1,607 +1,607 @@ -/*! - * express-session - * Copyright(c) 2010 Sencha Inc. - * Copyright(c) 2011 TJ Holowaychuk - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -/** - * Module dependencies. - * @private - */ - -var cookie = require('cookie'); -var crc = require('crc').crc32; -var debug = require('debug')('express-session'); -var deprecate = require('depd')('express-session'); -var parseUrl = require('parseurl'); -var uid = require('uid-safe').sync - , onHeaders = require('on-headers') - , signature = require('cookie-signature') - -var Session = require('./session/session') - , MemoryStore = require('./session/memory') - , Cookie = require('./session/cookie') - , Store = require('./session/store') - -// environment - -var env = process.env.NODE_ENV; - -/** - * Expose the middleware. - */ - -exports = module.exports = session; - -/** - * Expose constructors. - */ - -exports.Store = Store; -exports.Cookie = Cookie; -exports.Session = Session; -exports.MemoryStore = MemoryStore; - -/** - * Warning message for `MemoryStore` usage in production. - * @private - */ - -var warning = 'Warning: connect.session() MemoryStore is not\n' - + 'designed for a production environment, as it will leak\n' - + 'memory, and will not scale past a single process.'; - -/** - * Node.js 0.8+ async implementation. - * @private - */ - -/* istanbul ignore next */ -var defer = typeof setImmediate === 'function' - ? setImmediate - : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } - -/** - * Setup session store with the given `options`. - * - * @param {Object} [options] - * @param {Object} [options.cookie] Options for cookie - * @param {Function} [options.genid] - * @param {String} [options.name=connect.sid] Session ID cookie name - * @param {Boolean} [options.proxy] - * @param {Boolean} [options.resave] Resave unmodified sessions back to the store - * @param {Boolean} [options.rolling] Enable/disable rolling session expiration - * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store - * @param {String|Array} [options.secret] Secret for signing session ID - * @param {Object} [options.store=MemoryStore] Session store - * @param {String} [options.unset] - * @return {Function} middleware - * @public - */ - -function session(options){ - var options = options || {} - // name - previously "options.key" - , name = options.name || options.key || 'connect.sid' - , store = options.store || new MemoryStore - , cookie = options.cookie || {} - , trustProxy = options.proxy - , storeReady = true - , rollingSessions = options.rolling || false; - var resaveSession = options.resave; - var saveUninitializedSession = options.saveUninitialized; - var secret = options.secret; - - var generateId = options.genid || generateSessionId; - - if (typeof generateId !== 'function') { - throw new TypeError('genid option must be a function'); - } - - if (resaveSession === undefined) { - deprecate('undefined resave option; provide resave option'); - resaveSession = true; - } - - if (saveUninitializedSession === undefined) { - deprecate('undefined saveUninitialized option; provide saveUninitialized option'); - saveUninitializedSession = true; - } - - if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') { - throw new TypeError('unset option must be "destroy" or "keep"'); - } - - // TODO: switch to "destroy" on next major - var unsetDestroy = options.unset === 'destroy'; - - if (Array.isArray(secret) && secret.length === 0) { - throw new TypeError('secret option array must contain one or more strings'); - } - - if (secret && !Array.isArray(secret)) { - secret = [secret]; - } - - if (!secret) { - deprecate('req.secret; provide secret option'); - } - - // notify user that this store is not - // meant for a production environment - if ('production' == env && store instanceof MemoryStore) { - console.warn(warning); - } - - // generates the new session - store.generate = function(req){ - req.sessionID = generateId(req); - req.session = new Session(req); - req.session.cookie = new Cookie(cookie); - }; - - var storeImplementsTouch = typeof store.touch === 'function'; - store.on('disconnect', function(){ storeReady = false; }); - store.on('connect', function(){ storeReady = true; }); - - return function session(req, res, next) { - // self-awareness - if (req.session) return next(); - - // Handle connection as if there is no session if - // the store has temporarily disconnected etc - if (!storeReady) return debug('store is disconnected'), next(); - - // pathname mismatch - var originalPath = parseUrl.original(req).pathname; - if (0 != originalPath.indexOf(cookie.path || '/')) return next(); - - // ensure a secret is available or bail - if (!secret && !req.secret) { - next(new Error('secret option required for sessions')); - return; - } - - // backwards compatibility for signed cookies - // req.secret is passed from the cookie parser middleware - var secrets = secret || [req.secret]; - - var originalHash; - var originalId; - var savedHash; - - // expose store - req.sessionStore = store; - - // get the session ID from the cookie - var cookieId = req.sessionID = getcookie(req, name, secrets); - - // set-cookie - onHeaders(res, function(){ - if (!req.session) { - debug('no session'); - return; - } - - var cookie = req.session.cookie; - - // only send secure cookies via https - if (cookie.secure && !issecure(req, trustProxy)) { - debug('not secured'); - return; - } - - if (!shouldSetCookie(req)) { - return; - } - - setcookie(res, name, req.sessionID, secrets[0], cookie.data); - }); - - // proxy end() to commit the session - var _end = res.end; - var _write = res.write; - var ended = false; - res.end = function end(chunk, encoding) { - if (ended) { - return false; - } - - ended = true; - - var ret; - var sync = true; - - function writeend() { - if (sync) { - ret = _end.call(res, chunk, encoding); - sync = false; - return; - } - - _end.call(res); - } - - function writetop() { - if (!sync) { - return ret; - } - - if (chunk == null) { - ret = true; - return ret; - } - - var contentLength = Number(res.getHeader('Content-Length')); - - if (!isNaN(contentLength) && contentLength > 0) { - // measure chunk - chunk = !Buffer.isBuffer(chunk) - ? new Buffer(chunk, encoding) - : chunk; - encoding = undefined; - - if (chunk.length !== 0) { - debug('split response'); - ret = _write.call(res, chunk.slice(0, chunk.length - 1)); - chunk = chunk.slice(chunk.length - 1, chunk.length); - return ret; - } - } - - ret = _write.call(res, chunk, encoding); - sync = false; - - return ret; - } - - if (shouldDestroy(req)) { - // destroy session - debug('destroying'); - store.destroy(req.sessionID, function ondestroy(err) { - if (err) { - defer(next, err); - } - - debug('destroyed'); - writeend(); - }); - - return writetop(); - } - - // no session to save - if (!req.session) { - debug('no session'); - return _end.call(res, chunk, encoding); - } - - // touch session - req.session.touch(); - - if (shouldSave(req)) { - req.session.save(function onsave(err) { - if (err) { - defer(next, err); - } - - writeend(); - }); - - return writetop(); - } else if (storeImplementsTouch && shouldTouch(req)) { - // store implements touch method - debug('touching'); - store.touch(req.sessionID, req.session, function ontouch(err) { - if (err) { - defer(next, err); - } - - debug('touched'); - writeend(); - }); - - return writetop(); - } - - return _end.call(res, chunk, encoding); - }; - - // generate the session - function generate() { - store.generate(req); - originalId = req.sessionID; - originalHash = hash(req.session); - wrapmethods(req.session); - } - - // wrap session methods - function wrapmethods(sess) { - var _save = sess.save; - - function save() { - debug('saving %s', this.id); - savedHash = hash(this); - _save.apply(this, arguments); - } - - Object.defineProperty(sess, 'save', { - configurable: true, - enumerable: false, - value: save, - writable: true - }); - } - - // check if session has been modified - function isModified(sess) { - return originalId !== sess.id || originalHash !== hash(sess); - } - - // check if session has been saved - function isSaved(sess) { - return originalId === sess.id && savedHash === hash(sess); - } - - // determine if session should be destroyed - function shouldDestroy(req) { - return req.sessionID && unsetDestroy && req.session == null; - } - - // determine if session should be saved to store - function shouldSave(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - debug('session ignored because of bogus req.sessionID %o', req.sessionID); - return false; - } - - return !saveUninitializedSession && cookieId !== req.sessionID - ? isModified(req.session) - : !isSaved(req.session) - } - - // determine if session should be touched - function shouldTouch(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - debug('session ignored because of bogus req.sessionID %o', req.sessionID); - return false; - } - - return cookieId === req.sessionID && !shouldSave(req); - } - - // determine if cookie should be set on response - function shouldSetCookie(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - return false; - } - - // in case of rolling session, always reset the cookie - if (rollingSessions) { - return true; - } - - return cookieId != req.sessionID - ? saveUninitializedSession || isModified(req.session) - : req.session.cookie.expires != null && isModified(req.session); - } - - // generate a session if the browser doesn't send a sessionID - if (!req.sessionID) { - debug('no SID sent, generating session'); - generate(); - next(); - return; - } - - // generate the session object - debug('fetching %s', req.sessionID); - store.get(req.sessionID, function(err, sess){ - // error handling - if (err) { - debug('error %j', err); - - 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 - } - - wrapmethods(req.session); - } - - next(); - }); - }; -}; - -/** - * Generate a session ID for a new session. - * - * @return {String} - * @private - */ - -function generateSessionId(sess) { - return uid(24); -} - -/** - * Get the session ID cookie from request. - * - * @return {string} - * @private - */ - -function getcookie(req, name, secrets) { - var header = req.headers.cookie; - var raw; - var val; - - // read from cookie header - if (header) { - var cookies = cookie.parse(header); - - raw = cookies[name]; - - if (raw) { - if (raw.substr(0, 2) === 's:') { - val = unsigncookie(raw.slice(2), secrets); - - if (val === false) { - debug('cookie signature invalid'); - val = undefined; - } - } else { - debug('cookie unsigned') - } - } - } - - // back-compat read from cookieParser() signedCookies data - if (!val && req.signedCookies) { - val = req.signedCookies[name]; - - if (val) { - deprecate('cookie should be available in req.headers.cookie'); - } - } - - // back-compat read from cookieParser() cookies data - if (!val && req.cookies) { - raw = req.cookies[name]; - - if (raw) { - if (raw.substr(0, 2) === 's:') { - val = unsigncookie(raw.slice(2), secrets); - - if (val) { - deprecate('cookie should be available in req.headers.cookie'); - } - - if (val === false) { - debug('cookie signature invalid'); - val = undefined; - } - } else { - debug('cookie unsigned') - } - } - } - - return val; -} - -/** - * Hash the given `sess` object omitting changes to `.cookie`. - * - * @param {Object} sess - * @return {String} - * @private - */ - -function hash(sess) { - return crc(JSON.stringify(sess, function (key, val) { - if (key !== 'cookie') { - return val; - } - })); -} - -/** - * Determine if request is secure. - * - * @param {Object} req - * @param {Boolean} [trustProxy] - * @return {Boolean} - * @private - */ - -function issecure(req, trustProxy) { - // socket is https server - if (req.connection && req.connection.encrypted) { - return true; - } - - // do not trust proxy - if (trustProxy === false) { - return false; - } - - // no explicit trust; try req.secure from express - if (trustProxy !== true) { - var secure = req.secure; - return typeof secure === 'boolean' - ? secure - : false; - } - - // 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.toLowerCase().trim() - - return proto === 'https'; -} - -/** - * Set cookie on response. - * - * @private - */ - -function setcookie(res, name, val, secret, options) { - var signed = 's:' + signature.sign(val, secret); - var data = cookie.serialize(name, signed, options); - - debug('set-cookie %s', data); - - var prev = res.getHeader('set-cookie') || []; - var header = Array.isArray(prev) ? prev.concat(data) - : Array.isArray(data) ? [prev].concat(data) - : [prev, data]; - - res.setHeader('set-cookie', header) -} - -/** - * Verify and decode the given `val` with `secrets`. - * - * @param {String} val - * @param {Array} secrets - * @returns {String|Boolean} - * @private - */ -function unsigncookie(val, secrets) { - for (var i = 0; i < secrets.length; i++) { - var result = signature.unsign(val, secrets[i]); - - if (result !== false) { - return result; - } - } - - return false; -} +/*! + * express-session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Module dependencies. + * @private + */ + +var cookie = require('cookie'); +var crc = require('crc').crc32; +var debug = require('debug')('express-session'); +var deprecate = require('depd')('express-session'); +var parseUrl = require('parseurl'); +var uid = require('uid-safe').sync + , onHeaders = require('on-headers') + , signature = require('cookie-signature') + +var Session = require('./session/session') + , MemoryStore = require('./session/memory') + , Cookie = require('./session/cookie') + , Store = require('./session/store') + +// environment + +var env = process.env.NODE_ENV; + +/** + * Expose the middleware. + */ + +exports = module.exports = session; + +/** + * Expose constructors. + */ + +exports.Store = Store; +exports.Cookie = Cookie; +exports.Session = Session; +exports.MemoryStore = MemoryStore; + +/** + * Warning message for `MemoryStore` usage in production. + * @private + */ + +var warning = 'Warning: connect.session() MemoryStore is not\n' + + 'designed for a production environment, as it will leak\n' + + 'memory, and will not scale past a single process.'; + +/** + * Node.js 0.8+ async implementation. + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Setup session store with the given `options`. + * + * @param {Object} [options] + * @param {Object} [options.cookie] Options for cookie + * @param {Function} [options.genid] + * @param {String} [options.name=connect.sid] Session ID cookie name + * @param {Boolean} [options.proxy] + * @param {Boolean} [options.resave] Resave unmodified sessions back to the store + * @param {Boolean} [options.rolling] Enable/disable rolling session expiration + * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store + * @param {String|Array} [options.secret] Secret for signing session ID + * @param {Object} [options.store=MemoryStore] Session store + * @param {String} [options.unset] + * @return {Function} middleware + * @public + */ + +function session(options){ + var options = options || {} + // name - previously "options.key" + , name = options.name || options.key || 'connect.sid' + , store = options.store || new MemoryStore + , cookie = options.cookie || {} + , trustProxy = options.proxy + , storeReady = true + , rollingSessions = options.rolling || false; + var resaveSession = options.resave; + var saveUninitializedSession = options.saveUninitialized; + var secret = options.secret; + + var generateId = options.genid || generateSessionId; + + if (typeof generateId !== 'function') { + throw new TypeError('genid option must be a function'); + } + + if (resaveSession === undefined) { + deprecate('undefined resave option; provide resave option'); + resaveSession = true; + } + + if (saveUninitializedSession === undefined) { + deprecate('undefined saveUninitialized option; provide saveUninitialized option'); + saveUninitializedSession = true; + } + + if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') { + throw new TypeError('unset option must be "destroy" or "keep"'); + } + + // TODO: switch to "destroy" on next major + var unsetDestroy = options.unset === 'destroy'; + + if (Array.isArray(secret) && secret.length === 0) { + throw new TypeError('secret option array must contain one or more strings'); + } + + if (secret && !Array.isArray(secret)) { + secret = [secret]; + } + + if (!secret) { + deprecate('req.secret; provide secret option'); + } + + // notify user that this store is not + // meant for a production environment + if ('production' == env && store instanceof MemoryStore) { + console.warn(warning); + } + + // generates the new session + store.generate = function(req){ + req.sessionID = generateId(req); + req.session = new Session(req); + req.session.cookie = new Cookie(cookie); + }; + + var storeImplementsTouch = typeof store.touch === 'function'; + store.on('disconnect', function(){ storeReady = false; }); + store.on('connect', function(){ storeReady = true; }); + + return function session(req, res, next) { + // self-awareness + if (req.session) return next(); + + // Handle connection as if there is no session if + // the store has temporarily disconnected etc + if (!storeReady) return debug('store is disconnected'), next(); + + // pathname mismatch + var originalPath = parseUrl.original(req).pathname; + if (0 != originalPath.indexOf(cookie.path || '/')) return next(); + + // ensure a secret is available or bail + if (!secret && !req.secret) { + next(new Error('secret option required for sessions')); + return; + } + + // backwards compatibility for signed cookies + // req.secret is passed from the cookie parser middleware + var secrets = secret || [req.secret]; + + var originalHash; + var originalId; + var savedHash; + + // expose store + req.sessionStore = store; + + // get the session ID from the cookie + var cookieId = req.sessionID = getcookie(req, name, secrets); + + // set-cookie + onHeaders(res, function(){ + if (!req.session) { + debug('no session'); + return; + } + + var cookie = req.session.cookie; + + // only send secure cookies via https + if (cookie.secure && !issecure(req, trustProxy)) { + debug('not secured'); + return; + } + + if (!shouldSetCookie(req)) { + return; + } + + setcookie(res, name, req.sessionID, secrets[0], cookie.data); + }); + + // proxy end() to commit the session + var _end = res.end; + var _write = res.write; + var ended = false; + res.end = function end(chunk, encoding) { + if (ended) { + return false; + } + + ended = true; + + var ret; + var sync = true; + + function writeend() { + if (sync) { + ret = _end.call(res, chunk, encoding); + sync = false; + return; + } + + _end.call(res); + } + + function writetop() { + if (!sync) { + return ret; + } + + if (chunk == null) { + ret = true; + return ret; + } + + var contentLength = Number(res.getHeader('Content-Length')); + + if (!isNaN(contentLength) && contentLength > 0) { + // measure chunk + chunk = !Buffer.isBuffer(chunk) + ? new Buffer(chunk, encoding) + : chunk; + encoding = undefined; + + if (chunk.length !== 0) { + debug('split response'); + ret = _write.call(res, chunk.slice(0, chunk.length - 1)); + chunk = chunk.slice(chunk.length - 1, chunk.length); + return ret; + } + } + + ret = _write.call(res, chunk, encoding); + sync = false; + + return ret; + } + + if (shouldDestroy(req)) { + // destroy session + debug('destroying'); + store.destroy(req.sessionID, function ondestroy(err) { + if (err) { + defer(next, err); + } + + debug('destroyed'); + writeend(); + }); + + return writetop(); + } + + // no session to save + if (!req.session) { + debug('no session'); + return _end.call(res, chunk, encoding); + } + + // touch session + req.session.touch(); + + if (shouldSave(req)) { + req.session.save(function onsave(err) { + if (err) { + defer(next, err); + } + + writeend(); + }); + + return writetop(); + } else if (storeImplementsTouch && shouldTouch(req)) { + // store implements touch method + debug('touching'); + store.touch(req.sessionID, req.session, function ontouch(err) { + if (err) { + defer(next, err); + } + + debug('touched'); + writeend(); + }); + + return writetop(); + } + + return _end.call(res, chunk, encoding); + }; + + // generate the session + function generate() { + store.generate(req); + originalId = req.sessionID; + originalHash = hash(req.session); + wrapmethods(req.session); + } + + // wrap session methods + function wrapmethods(sess) { + var _save = sess.save; + + function save() { + debug('saving %s', this.id); + savedHash = hash(this); + _save.apply(this, arguments); + } + + Object.defineProperty(sess, 'save', { + configurable: true, + enumerable: false, + value: save, + writable: true + }); + } + + // check if session has been modified + function isModified(sess) { + return originalId !== sess.id || originalHash !== hash(sess); + } + + // check if session has been saved + function isSaved(sess) { + return originalId === sess.id && savedHash === hash(sess); + } + + // determine if session should be destroyed + function shouldDestroy(req) { + return req.sessionID && unsetDestroy && req.session == null; + } + + // determine if session should be saved to store + function shouldSave(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } + + return !saveUninitializedSession && cookieId !== req.sessionID + ? isModified(req.session) + : !isSaved(req.session) + } + + // determine if session should be touched + function shouldTouch(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } + + return cookieId === req.sessionID && !shouldSave(req); + } + + // determine if cookie should be set on response + function shouldSetCookie(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + return false; + } + + // in case of rolling session, always reset the cookie + if (rollingSessions) { + return true; + } + + return cookieId != req.sessionID + ? saveUninitializedSession || isModified(req.session) + : req.session.cookie.expires != null && isModified(req.session); + } + + // generate a session if the browser doesn't send a sessionID + if (!req.sessionID) { + debug('no SID sent, generating session'); + generate(); + next(); + return; + } + + // generate the session object + debug('fetching %s', req.sessionID); + store.get(req.sessionID, function(err, sess){ + // error handling + if (err) { + debug('error %j', err); + + 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 + } + + wrapmethods(req.session); + } + + next(); + }); + }; +}; + +/** + * Generate a session ID for a new session. + * + * @return {String} + * @private + */ + +function generateSessionId(sess) { + return uid(24); +} + +/** + * Get the session ID cookie from request. + * + * @return {string} + * @private + */ + +function getcookie(req, name, secrets) { + var header = req.headers.cookie; + var raw; + var val; + + // read from cookie header + if (header) { + var cookies = cookie.parse(header); + + raw = cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } + } + } + + // back-compat read from cookieParser() signedCookies data + if (!val && req.signedCookies) { + val = req.signedCookies[name]; + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + } + + // back-compat read from cookieParser() cookies data + if (!val && req.cookies) { + raw = req.cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } + } + } + + return val; +} + +/** + * Hash the given `sess` object omitting changes to `.cookie`. + * + * @param {Object} sess + * @return {String} + * @private + */ + +function hash(sess) { + return crc(JSON.stringify(sess, function (key, val) { + if (key !== 'cookie') { + return val; + } + })); +} + +/** + * Determine if request is secure. + * + * @param {Object} req + * @param {Boolean} [trustProxy] + * @return {Boolean} + * @private + */ + +function issecure(req, trustProxy) { + // socket is https server + if (req.connection && req.connection.encrypted) { + return true; + } + + // do not trust proxy + if (trustProxy === false) { + return false; + } + + // no explicit trust; try req.secure from express + if (trustProxy !== true) { + var secure = req.secure; + return typeof secure === 'boolean' + ? secure + : false; + } + + // 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.toLowerCase().trim() + + return proto === 'https'; +} + +/** + * Set cookie on response. + * + * @private + */ + +function setcookie(res, name, val, secret, options) { + var signed = 's:' + signature.sign(val, secret); + var data = cookie.serialize(name, signed, options); + + debug('set-cookie %s', data); + + var prev = res.getHeader('set-cookie') || []; + var header = Array.isArray(prev) ? prev.concat(data) + : Array.isArray(data) ? [prev].concat(data) + : [prev, data]; + + res.setHeader('set-cookie', header) +} + +/** + * Verify and decode the given `val` with `secrets`. + * + * @param {String} val + * @param {Array} secrets + * @returns {String|Boolean} + * @private + */ +function unsigncookie(val, secrets) { + for (var i = 0; i < secrets.length; i++) { + var result = signature.unsign(val, secrets[i]); + + if (result !== false) { + return result; + } + } + + return false; +} diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 00000000..9e7cd7be --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,7 @@ +include.path=${php.global.include.path} +php.version=PHP_56 +source.encoding=UTF-8 +src.dir=. +tags.asp=false +tags.short=false +web.root=. diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 00000000..94768ade --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.php.project + + + PhpProject1 + + + diff --git a/package.json b/package.json index b0c3108f..bd9c8cdd 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,45 @@ -{ - "name": "express-session", - "version": "1.11.3", - "description": "Simple session middleware for Express", - "author": "TJ Holowaychuk (http://tjholowaychuk.com)", - "contributors": [ - "Douglas Christopher Wilson ", - "Joe Wagner " - ], - "repository": "expressjs/session", - "license": "MIT", - "dependencies": { - "cookie": "0.1.3", - "cookie-signature": "1.0.6", - "crc": "3.3.0", - "debug": "~2.2.0", - "depd": "~1.0.1", - "on-headers": "~1.0.0", - "parseurl": "~1.3.0", - "uid-safe": "~2.0.0", - "utils-merge": "1.0.0" - }, - "devDependencies": { - "after": "0.8.1", - "cookie-parser": "~1.3.5", - "express": "~4.13.1", - "istanbul": "0.3.17", - "mocha": "2.2.5", - "supertest": "1.0.1" - }, - "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/" - } -} +{ + "name": "express-session", + "version": "1.11.3", + "description": "Simple session middleware for Express", + "author": "TJ Holowaychuk (http://tjholowaychuk.com)", + "contributors": [ + "Douglas Christopher Wilson ", + "Joe Wagner " + ], + "repository": "expressjs/session", + "license": "MIT", + "dependencies": { + "cookie": "0.1.3", + "cookie-signature": "1.0.6", + "crc": "3.3.0", + "debug": "~2.2.0", + "depd": "~1.0.1", + "on-headers": "~1.0.0", + "parseurl": "~1.3.0", + "uid-safe": "~2.0.0", + "utils-merge": "1.0.0" + }, + "devDependencies": { + "after": "0.8.1", + "cookie-parser": "~1.3.5", + "express": "~4.13.1", + "istanbul": "0.3.17", + "mocha": "2.2.5", + "supertest": "1.0.1" + }, + "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/" + } +} diff --git a/session/cookie.js b/session/cookie.js index 86591de3..83950dce 100644 --- a/session/cookie.js +++ b/session/cookie.js @@ -1,128 +1,128 @@ - -/*! - * Connect - session - Cookie - * Copyright(c) 2010 Sencha Inc. - * Copyright(c) 2011 TJ Holowaychuk - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var merge = require('utils-merge') - , cookie = require('cookie'); - -/** - * Initialize a new `Cookie` with the given `options`. - * - * @param {IncomingMessage} req - * @param {Object} options - * @api private - */ - -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; -}; - -/*! - * Prototype. - */ - -Cookie.prototype = { - - /** - * Set expires `date`. - * - * @param {Date} date - * @api public - */ - - set expires(date) { - this._expires = date; - this.originalMaxAge = this.maxAge; - }, - - /** - * Get expires `date`. - * - * @return {Date} - * @api public - */ - - get expires() { - return this._expires; - }, - - /** - * Set expires via max-age in `ms`. - * - * @param {Number} ms - * @api public - */ - - set maxAge(ms) { - this.expires = 'number' == typeof ms - ? new Date(Date.now() + ms) - : ms; - }, - - /** - * Get expires max-age in `ms`. - * - * @return {Number} - * @api public - */ - - get maxAge() { - return this.expires instanceof Date - ? this.expires.valueOf() - Date.now() - : this.expires; - }, - - /** - * Return cookie data object. - * - * @return {Object} - * @api private - */ - - get data() { - return { - originalMaxAge: this.originalMaxAge - , expires: this._expires - , secure: this.secure - , httpOnly: this.httpOnly - , domain: this.domain - , path: this.path - } - }, - - /** - * Return a serialized cookie string. - * - * @return {String} - * @api public - */ - - serialize: function(name, val){ - return cookie.serialize(name, val, this.data); - }, - - /** - * Return JSON representation of this cookie. - * - * @return {Object} - * @api private - */ - - toJSON: function(){ - return this.data; - } -}; + +/*! + * Connect - session - Cookie + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var merge = require('utils-merge') + , cookie = require('cookie'); + +/** + * Initialize a new `Cookie` with the given `options`. + * + * @param {IncomingMessage} req + * @param {Object} options + * @api private + */ + +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; +}; + +/*! + * Prototype. + */ + +Cookie.prototype = { + + /** + * Set expires `date`. + * + * @param {Date} date + * @api public + */ + + set expires(date) { + this._expires = date; + this.originalMaxAge = this.maxAge; + }, + + /** + * Get expires `date`. + * + * @return {Date} + * @api public + */ + + get expires() { + return this._expires; + }, + + /** + * Set expires via max-age in `ms`. + * + * @param {Number} ms + * @api public + */ + + set maxAge(ms) { + this.expires = 'number' == typeof ms + ? new Date(Date.now() + ms) + : ms; + }, + + /** + * Get expires max-age in `ms`. + * + * @return {Number} + * @api public + */ + + get maxAge() { + return this.expires instanceof Date + ? this.expires.valueOf() - Date.now() + : this.expires; + }, + + /** + * Return cookie data object. + * + * @return {Object} + * @api private + */ + + get data() { + return { + originalMaxAge: this.originalMaxAge + , expires: this._expires + , secure: this.secure + , httpOnly: this.httpOnly + , domain: this.domain + , path: this.path + } + }, + + /** + * Return a serialized cookie string. + * + * @return {String} + * @api public + */ + + serialize: function(name, val){ + return cookie.serialize(name, val, this.data); + }, + + /** + * Return JSON representation of this cookie. + * + * @return {Object} + * @api private + */ + + toJSON: function(){ + return this.data; + } +}; diff --git a/session/memory.js b/session/memory.js index 9887680d..164430b1 100644 --- a/session/memory.js +++ b/session/memory.js @@ -1,183 +1,183 @@ -/*! - * express-session - * Copyright(c) 2010 Sencha Inc. - * Copyright(c) 2011 TJ Holowaychuk - * Copyright(c) 2015 Douglas Christopher Wilson - * MIT Licensed - */ - -/** - * Module dependencies. - * @private - */ - -var Store = require('./store') -var util = require('util') - -/** - * Shim setImmediate for node.js < 0.10 - * @private - */ - -/* istanbul ignore next */ -var defer = typeof setImmediate === 'function' - ? setImmediate - : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } - -/** - * Module exports. - */ - -module.exports = MemoryStore - -/** - * A session store in memory. - * @public - */ - -function MemoryStore() { - Store.call(this) - this.sessions = Object.create(null) -} - -/** - * Inherit from Store. - */ - -util.inherits(MemoryStore, Store) - -/** - * Get all active sessions. - * - * @param {function} callback - * @public - */ - -MemoryStore.prototype.all = function all(callback) { - var sessionIds = Object.keys(this.sessions) - var sessions = Object.create(null) - - for (var i = 0; i < sessionIds.length; i++) { - var sessionId = sessionIds[i] - var session = getSession.call(this, sessionId) - - if (session) { - sessions[sessionId] = session; - } - } - - callback && defer(callback, null, sessions) -} - -/** - * Clear all sessions. - * - * @param {function} callback - * @public - */ - -MemoryStore.prototype.clear = function clear(callback) { - this.sessions = Object.create(null) - callback && defer(callback) -} - -/** - * Destroy the session associated with the given session ID. - * - * @param {string} sessionId - * @public - */ - -MemoryStore.prototype.destroy = function destroy(sessionId, callback) { - delete this.sessions[sessionId] - callback && defer(callback) -} - -/** - * Fetch session by the given session ID. - * - * @param {string} sessionId - * @param {function} callback - * @public - */ - -MemoryStore.prototype.get = function get(sessionId, callback) { - defer(callback, null, getSession.call(this, sessionId)) -} - -/** - * Commit the given session associated with the given sessionId to the store. - * - * @param {string} sessionId - * @param {object} session - * @param {function} callback - * @public - */ - -/** - * Get number of active sessions. - * - * @param {function} callback - * @public - */ - -MemoryStore.prototype.length = function length(callback) { - this.all(function (err, sessions) { - if (err) return callback(err) - callback(null, Object.keys(sessions).length) - }) -} - -MemoryStore.prototype.set = function set(sessionId, session, callback) { - this.sessions[sessionId] = JSON.stringify(session) - callback && defer(callback) -} - -/** - * Touch the given session object associated with the given session ID. - * - * @param {string} sessionId - * @param {object} session - * @param {function} callback - * @public - */ - -MemoryStore.prototype.touch = function touch(sessionId, session, callback) { - var currentSession = getSession.call(this, sessionId) - - if (currentSession) { - // update expiration - currentSession.cookie = session.cookie - this.sessions[sessionId] = JSON.stringify(currentSession) - } - - callback && defer(callback) -} - -/** - * Get session from the store. - * @private - */ - -function getSession(sessionId) { - var sess = this.sessions[sessionId] - - if (!sess) { - return - } - - // 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 - } - - return sess -} +/*! + * express-session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Module dependencies. + * @private + */ + +var Store = require('./store') +var util = require('util') + +/** + * Shim setImmediate for node.js < 0.10 + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Module exports. + */ + +module.exports = MemoryStore + +/** + * A session store in memory. + * @public + */ + +function MemoryStore() { + Store.call(this) + this.sessions = Object.create(null) +} + +/** + * Inherit from Store. + */ + +util.inherits(MemoryStore, Store) + +/** + * Get all active sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.all = function all(callback) { + var sessionIds = Object.keys(this.sessions) + var sessions = Object.create(null) + + for (var i = 0; i < sessionIds.length; i++) { + var sessionId = sessionIds[i] + var session = getSession.call(this, sessionId) + + if (session) { + sessions[sessionId] = session; + } + } + + callback && defer(callback, null, sessions) +} + +/** + * Clear all sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.clear = function clear(callback) { + this.sessions = Object.create(null) + callback && defer(callback) +} + +/** + * Destroy the session associated with the given session ID. + * + * @param {string} sessionId + * @public + */ + +MemoryStore.prototype.destroy = function destroy(sessionId, callback) { + delete this.sessions[sessionId] + callback && defer(callback) +} + +/** + * Fetch session by the given session ID. + * + * @param {string} sessionId + * @param {function} callback + * @public + */ + +MemoryStore.prototype.get = function get(sessionId, callback) { + defer(callback, null, getSession.call(this, sessionId)) +} + +/** + * Commit the given session associated with the given sessionId to the store. + * + * @param {string} sessionId + * @param {object} session + * @param {function} callback + * @public + */ + +/** + * Get number of active sessions. + * + * @param {function} callback + * @public + */ + +MemoryStore.prototype.length = function length(callback) { + this.all(function (err, sessions) { + if (err) return callback(err) + callback(null, Object.keys(sessions).length) + }) +} + +MemoryStore.prototype.set = function set(sessionId, session, callback) { + this.sessions[sessionId] = JSON.stringify(session) + callback && defer(callback) +} + +/** + * Touch the given session object associated with the given session ID. + * + * @param {string} sessionId + * @param {object} session + * @param {function} callback + * @public + */ + +MemoryStore.prototype.touch = function touch(sessionId, session, callback) { + var currentSession = getSession.call(this, sessionId) + + if (currentSession) { + // update expiration + currentSession.cookie = session.cookie + this.sessions[sessionId] = JSON.stringify(currentSession) + } + + callback && defer(callback) +} + +/** + * Get session from the store. + * @private + */ + +function getSession(sessionId) { + var sess = this.sessions[sessionId] + + if (!sess) { + return + } + + // 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 + } + + return sess +} diff --git a/session/session.js b/session/session.js index 4647f9da..65f32b6f 100644 --- a/session/session.js +++ b/session/session.js @@ -1,124 +1,124 @@ - -/*! - * Connect - session - Session - * Copyright(c) 2010 Sencha Inc. - * Copyright(c) 2011 TJ Holowaychuk - * MIT Licensed - */ - -/** - * Expose Session. - */ - -module.exports = Session; - -/** - * Create a new `Session` with the given request and `data`. - * - * @param {IncomingRequest} req - * @param {Object} data - * @api private - */ - -function Session(req, data) { - Object.defineProperty(this, 'req', { value: req }); - Object.defineProperty(this, 'id', { value: req.sessionID }); - - if (typeof data === 'object' && data !== null) { - // merge data into this, ignoring prototype properties - for (var prop in data) { - if (!(prop in this)) { - this[prop] = data[prop] - } - } - } -} - -/** - * Update reset `.cookie.maxAge` to prevent - * the cookie from expiring when the - * session is still active. - * - * @return {Session} for chaining - * @api public - */ - -Session.prototype.touch = function(){ - return this.resetMaxAge(); -}; - -/** - * Reset `.maxAge` to `.originalMaxAge`. - * - * @return {Session} for chaining - * @api public - */ - -Session.prototype.resetMaxAge = function(){ - this.cookie.maxAge = this.cookie.originalMaxAge; - return this; -}; - -/** - * Save the session data with optional callback `fn(err)`. - * - * @param {Function} fn - * @return {Session} for chaining - * @api public - */ - -Session.prototype.save = function(fn){ - this.req.sessionStore.set(this.id, this, fn || function(){}); - return this; -}; - -/** - * Re-loads the session data _without_ altering - * the maxAge properties. Invokes the callback `fn(err)`, - * after which time if no exception has occurred the - * `req.session` property will be a new `Session` object, - * although representing the same session. - * - * @param {Function} fn - * @return {Session} for chaining - * @api public - */ - -Session.prototype.reload = function(fn){ - var req = this.req - , 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')); - store.createSession(req, sess); - fn(); - }); - return this; -}; - -/** - * Destroy `this` session. - * - * @param {Function} fn - * @return {Session} for chaining - * @api public - */ - -Session.prototype.destroy = function(fn){ - delete this.req.session; - this.req.sessionStore.destroy(this.id, fn); - return this; -}; - -/** - * Regenerate this request's session. - * - * @param {Function} fn - * @return {Session} for chaining - * @api public - */ - -Session.prototype.regenerate = function(fn){ - this.req.sessionStore.regenerate(this.req, fn); - return this; -}; + +/*! + * Connect - session - Session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Expose Session. + */ + +module.exports = Session; + +/** + * Create a new `Session` with the given request and `data`. + * + * @param {IncomingRequest} req + * @param {Object} data + * @api private + */ + +function Session(req, data) { + Object.defineProperty(this, 'req', { value: req }); + Object.defineProperty(this, 'id', { value: req.sessionID }); + + if (typeof data === 'object' && data !== null) { + // merge data into this, ignoring prototype properties + for (var prop in data) { + if (!(prop in this)) { + this[prop] = data[prop] + } + } + } +} + +/** + * Update reset `.cookie.maxAge` to prevent + * the cookie from expiring when the + * session is still active. + * + * @return {Session} for chaining + * @api public + */ + +Session.prototype.touch = function(){ + return this.resetMaxAge(); +}; + +/** + * Reset `.maxAge` to `.originalMaxAge`. + * + * @return {Session} for chaining + * @api public + */ + +Session.prototype.resetMaxAge = function(){ + this.cookie.maxAge = this.cookie.originalMaxAge; + return this; +}; + +/** + * Save the session data with optional callback `fn(err)`. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.save = function(fn){ + this.req.sessionStore.set(this.id, this, fn || function(){}); + return this; +}; + +/** + * Re-loads the session data _without_ altering + * the maxAge properties. Invokes the callback `fn(err)`, + * after which time if no exception has occurred the + * `req.session` property will be a new `Session` object, + * although representing the same session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.reload = function(fn){ + var req = this.req + , 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')); + store.createSession(req, sess); + fn(); + }); + return this; +}; + +/** + * Destroy `this` session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.destroy = function(fn){ + delete this.req.session; + this.req.sessionStore.destroy(this.id, fn); + return this; +}; + +/** + * Regenerate this request's session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.regenerate = function(fn){ + this.req.sessionStore.regenerate(this.req, fn); + return this; +}; diff --git a/session/store.js b/session/store.js index 54294cbd..66816328 100644 --- a/session/store.js +++ b/session/store.js @@ -1,84 +1,84 @@ - -/*! - * Connect - session - Store - * Copyright(c) 2010 Sencha Inc. - * Copyright(c) 2011 TJ Holowaychuk - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var EventEmitter = require('events').EventEmitter - , Session = require('./session') - , Cookie = require('./cookie'); - -/** - * Initialize abstract `Store`. - * - * @api private - */ - -var Store = module.exports = function Store(options){}; - -/** - * Inherit from `EventEmitter.prototype`. - */ - -Store.prototype.__proto__ = EventEmitter.prototype; - -/** - * Re-generate the given requests's session. - * - * @param {IncomingRequest} req - * @return {Function} fn - * @api public - */ - -Store.prototype.regenerate = function(req, fn){ - var self = this; - this.destroy(req.sessionID, function(err){ - self.generate(req); - fn(err); - }); -}; - -/** - * Load a `Session` instance via the given `sid` - * and invoke the callback `fn(err, sess)`. - * - * @param {String} sid - * @param {Function} fn - * @api public - */ - -Store.prototype.load = function(sid, fn){ - var self = this; - this.get(sid, function(err, sess){ - if (err) return fn(err); - if (!sess) return fn(); - var req = { sessionID: sid, sessionStore: self }; - sess = self.createSession(req, sess); - fn(null, sess); - }); -}; - -/** - * Create session from JSON `sess` data. - * - * @param {IncomingRequest} req - * @param {Object} sess - * @return {Session} - * @api private - */ - -Store.prototype.createSession = function(req, sess){ - var expires = sess.cookie.expires - , orig = sess.cookie.originalMaxAge; - sess.cookie = new Cookie(sess.cookie); - if ('string' == typeof expires) sess.cookie.expires = new Date(expires); - sess.cookie.originalMaxAge = orig; - req.session = new Session(req, sess); - return req.session; -}; + +/*! + * Connect - session - Store + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter + , Session = require('./session') + , Cookie = require('./cookie'); + +/** + * Initialize abstract `Store`. + * + * @api private + */ + +var Store = module.exports = function Store(options){}; + +/** + * Inherit from `EventEmitter.prototype`. + */ + +Store.prototype.__proto__ = EventEmitter.prototype; + +/** + * Re-generate the given requests's session. + * + * @param {IncomingRequest} req + * @return {Function} fn + * @api public + */ + +Store.prototype.regenerate = function(req, fn){ + var self = this; + this.destroy(req.sessionID, function(err){ + self.generate(req); + fn(err); + }); +}; + +/** + * Load a `Session` instance via the given `sid` + * and invoke the callback `fn(err, sess)`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + +Store.prototype.load = function(sid, fn){ + var self = this; + this.get(sid, function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(); + var req = { sessionID: sid, sessionStore: self }; + sess = self.createSession(req, sess); + fn(null, sess); + }); +}; + +/** + * Create session from JSON `sess` data. + * + * @param {IncomingRequest} req + * @param {Object} sess + * @return {Session} + * @api private + */ + +Store.prototype.createSession = function(req, sess){ + var expires = sess.cookie.expires + , orig = sess.cookie.originalMaxAge; + sess.cookie = new Cookie(sess.cookie); + if ('string' == typeof expires) sess.cookie.expires = new Date(expires); + sess.cookie.originalMaxAge = orig; + req.session = new Session(req, sess); + return req.session; +}; diff --git a/test/fixtures/server.crt b/test/fixtures/server.crt index c019198a..14e7499f 100644 --- a/test/fixtures/server.crt +++ b/test/fixtures/server.crt @@ -1,14 +1,14 @@ ------BEGIN CERTIFICATE----- -MIICMzCCAZwCCQCJTms0qcIZgDANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQGEwJV -UzEQMA4GA1UECBMHSW5kaWFuYTEdMBsGA1UEChMUbm9kZS1leHByZXNzLXNlc3Np -b24xHjAcBgNVBAMTFWV4cHJlc3Mtc2Vzc2lvbi5sb2NhbDAeFw0xNDExMjMwNTQ3 -MzlaFw0yNDExMjAwNTQ3MzlaMF4xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdJbmRp -YW5hMR0wGwYDVQQKExRub2RlLWV4cHJlc3Mtc2Vzc2lvbjEeMBwGA1UEAxMVZXhw -cmVzcy1zZXNzaW9uLmxvY2FsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDY -G398zqN6Yv/FAx77eoLLje4VNCYDXUciWBzceaZ5u/rtP/XshTQWDKFfUbb/kmni -DazsCasSoNsUCrcqQPJF1jF3F9XjsZxcggDab8MAbAMhnhJax2l3yPJsWnB/8mrY -cdsdQNPXJxQW9MMZgUxz73gIY5/rEeayU9owgcnVcQIDAQABMA0GCSqGSIb3DQEB -BQUAA4GBADi0XXsZlQAKxOyD4qvdJNWJXlfqhr1q53dZmfF7nikDaMDiMspczTS/ -pxNbq2UaMc7g+6qmXPaPQoN3laQQtMdyVfSh6EIJHbzXXzdcCT4fDBYX7iwvh0Gg -DUpjmxmCzFCaob9+hZzwbJi5MFQ4Qq12LW1aYHMs8wgLboHml1WL ------END CERTIFICATE----- +-----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 index dabf2b83..b83c6a3f 100644 --- a/test/fixtures/server.key +++ b/test/fixtures/server.key @@ -1,15 +1,15 @@ ------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----- +-----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 f592ef3f..f8f68e9c 100644 --- a/test/session.js +++ b/test/session.js @@ -1,2122 +1,2122 @@ - -process.env.NO_DEPRECATION = 'express-session'; - -var after = require('after') -var assert = require('assert') -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 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') - }) - - 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); - - request(app) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }) - - it('should error without secret', function(done){ - request(createServer({ secret: undefined })) - .get('/') - .expect(500, /secret.*required/, done) - }) - - 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'); - - request(app) - .get('/') - .expect(200, '', done) - }) - - it('should create a new session', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - req.session.active = true - res.end('session active') - }); - - request(server) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'session active', function (err, res) { - if (err) return done(err) - store.length(function (err, len) { - if (err) return done(err) - assert.equal(len, 1) - done() - }) - }) - }) - - it('should load session from cookie sid', function (done) { - var count = 0 - var server = createServer(null, 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) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'session 1', done) - }) - }) - - it('should pass session fetch error', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - res.end('hello, world') - }) - - store.get = function destroy(sid, callback) { - callback(new Error('boom!')) - } - - request(server) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'hello, world', function (err, res) { - if (err) return done(err) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(500, 'boom!', done) - }) - }) - - it('should treat ENOENT session fetch error as not found', 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) - }) - - store.get = function destroy(sid, callback) { - var err = new Error('boom!') - err.code = 'ENOENT' - callback(err) - } - - request(server) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'session 1', function (err, res) { - if (err) return done(err) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'session 2', done) - }) - }) - - it('should create multiple sessions', function (done) { - var cb = after(2, check) - var count = 0 - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - var isnew = req.session.num === undefined - req.session.num = req.session.num || ++count - res.end('session ' + (isnew ? 'created' : 'updated')) - }); - - function check(err) { - if (err) return done(err) - store.all(function (err, sess) { - if (err) return done(err) - assert.equal(Object.keys(sess).length, 2) - done() - }) - } - - request(server) - .get('/') - .expect(200, 'session created', cb) - - request(server) - .get('/') - .expect(200, 'session created', cb) - }) - - 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'); - - request(app) - .get('/') - .expect('Content-Type', 'text/plain') - .expect(200, 'Hello, world!', done); - }) - - it('should handle res.end(null) calls', function (done) { - var server = createServer(null, function (req, res) { - res.end(null) - }) - - request(server) - .get('/') - .expect(200, '', done) - }) - - it('should handle reserved properties in storage', function (done) { - var count = 0 - var sid - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - sid = req.session.id - req.session.num = req.session.num || ++count - res.end('session saved') - }) - - request(server) - .get('/') - .expect(200, 'session saved', function (err, res) { - if (err) return done(err) - store.get(sid, function (err, sess) { - if (err) return done(err) - // save is reserved - sess.save = 'nope' - store.set(sid, sess, function (err) { - if (err) return done(err) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'session saved', done) - }) - }) - }) - }) - - it('should only have session data enumerable (and cookie)', function (done) { - var server = createServer(null, function (req, res) { - req.session.test1 = 1 - req.session.test2 = 'b' - res.end(Object.keys(req.session).sort().join(',')) - }) - - request(server) - .get('/') - .expect(200, 'cookie,test1,test2', done) - }) - - it('should not save with bogus req.sessionID', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - req.sessionID = function () {} - req.session.test1 = 1 - req.session.test2 = 'b' - res.end() - }) - - request(server) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, function (err) { - if (err) return done(err) - store.length(function (err, length) { - if (err) return done(err) - assert.equal(length, 0) - done() - }) - }) - }) - - 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() - }) - }) - - 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 - res.setHeader('Content-Length', '0') - 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() - }) - }) - - 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 - res.setHeader('Content-Length', '12') - res.write('hello, ') - 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() - }) - }) - - 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 - res.setHeader('Content-Length', '13') - 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() - }) - }) - }) - - describe('when sid not in store', function () { - it('should create a new session', 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.clear(function (err) { - if (err) return done(err) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'session 2', done) - }) - }) - }) - - it('should have a new sid', 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) - 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() - }) - }) - }) - }) - }) - - describe('when sid not properly signed', function () { - it('should generate new session', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store, key: 'sessid' }, function (req, res) { - var isnew = req.session.active === undefined - req.session.active = true - res.end('session ' + (isnew ? 'created' : 'read')) - }) - - request(server) - .get('/') - .expect(shouldSetCookie('sessid')) - .expect(200, 'session created', function (err, res) { - if (err) return done(err) - var val = sid(res) - assert.ok(val) - request(server) - .get('/') - .set('Cookie', 'sessid=' + val) - .expect(shouldSetCookie('sessid')) - .expect(200, 'session created', done) - }) - }) - - it('should not attempt fetch from store', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store, key: 'sessid' }, function (req, res) { - var isnew = req.session.active === undefined - req.session.active = true - res.end('session ' + (isnew ? 'created' : 'read')) - }) - - request(server) - .get('/') - .expect(shouldSetCookie('sessid')) - .expect(200, 'session created', function (err, res) { - if (err) return done(err) - var val = cookie(res).replace(/...\./, '.') - - assert.ok(val) - request(server) - .get('/') - .set('Cookie', val) - .expect(shouldSetCookie('sessid')) - .expect(200, 'session created', done) - }) - }) - }) - - describe('when session expired in store', function () { - it('should create a new session', function (done) { - var count = 0 - var store = new session.MemoryStore() - var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) - setTimeout(function () { - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'session 2', done) - }, 20) - }) - }) - - it('should have a new sid', function (done) { - var count = 0 - var store = new session.MemoryStore() - var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) - 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() - }) - }, 15) - }) - }) - - it('should not exist in store', function (done) { - var count = 0 - var store = new session.MemoryStore() - var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) - setTimeout(function () { - store.all(function (err, sess) { - if (err) return done(err) - assert.equal(Object.keys(sess).length, 0) - done() - }) - }, 10) - }) - }) - }) - - describe('proxy option', function(){ - describe('when enabled', function(){ - var server - before(function () { - server = createServer({ proxy: true, cookie: { secure: true, maxAge: 5 }}) - }) - - it('should trust X-Forwarded-Proto when string', function(done){ - request(server) - .get('/') - .set('X-Forwarded-Proto', 'https') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }) - - it('should trust X-Forwarded-Proto when comma-separated list', function(done){ - request(server) - .get('/') - .set('X-Forwarded-Proto', 'https,http') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }) - - it('should work when no header', function(done){ - request(server) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }) - }) - - describe('when disabled', function(){ - var server - before(function () { - server = createServer({ proxy: false, cookie: { secure: true, maxAge: 5 }}) - }) - - it('should not trust X-Forwarded-Proto', function(done){ - request(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) - .get('/') - .set('X-Forwarded-Proto', 'https') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, 'true', done) - }) - }) - - describe('when unspecified', function(){ - var server - before(function () { - server = createServer({ cookie: { secure: true, maxAge: 5 }}) - }) - - it('should not trust X-Forwarded-Proto', function(done){ - request(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) - .get('/') - .set('X-Forwarded-Proto', 'https') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'true', 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) - }); - - 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('key option', function(){ - it('should default to "connect.sid"', function(done){ - request(createServer()) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done) - }) - - it('should allow overriding', function(done){ - request(createServer({ key: 'session_id' })) - .get('/') - .expect(shouldSetCookie('session_id')) - .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){ - var save = req.session.save; - req.session.user = 'bob'; - res.end(); - }); - - 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 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){ - var save = req.session.save; - req.session.user = 'bob'; - res.end(); - }); - - 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) - }); - }); - }); - - 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(); - }); - - request(app) - .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', '2') - .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(); - }); - - request(app) - .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', '2') - .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(); - }); - - request(app) - .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); - }); - }); - - 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(); - }); - - request(app) - .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', '2') - .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') - }) - - store.touch = function touch(sid, sess, callback) { - callback(new Error('boom!')) - } - - server.on('error', function onerror(err) { - assert.ok(err) - assert.equal(err.message, 'boom!') - cb() - }) - - request(server) - .get('/') - .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(); - }); - - request(app) - .get('/') - .expect('x-count', '1') - .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(); - }); - - request(app) - .get('/') - .expect('x-count', '1') - .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(); - }); - - request(app) - .get('/') - .expect('x-count', '0') - .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(); - }); - - request(app) - .get('/') - .expect('x-count', '1') - .expect(shouldSetCookie('connect.sid')) - .expect(200, done); - }); - - it('should pass session save error', function (done) { - var cb = after(2, done) - var store = new session.MemoryStore() - var server = createServer({ store: store, saveUninitialized: true }, function (req, res) { - res.end('session saved') - }) - - store.set = function destroy(sid, sess, callback) { - callback(new Error('boom!')) - } - - server.on('error', function onerror(err) { - assert.ok(err) - assert.equal(err.message, 'boom!') - cb() - }) - - request(server) - .get('/') - .expect(200, 'session saved', cb) - }) - - it('should prevent uninitialized session from being touched', function (done) { - var cb = after(1, done) - var store = new session.MemoryStore() - var server = createServer({ saveUninitialized: false, store: store, cookie: { maxAge: min } }, function (req, res) { - res.end() - }) - - store.touch = function () { - cb(new Error('should not be called')) - } - - request(server) - .get('/') - .expect(200, cb) - }) - }); - - describe('secret option', function () { - it('should reject empty arrays', function () { - assert.throws(createServer.bind(null, { secret: [] }), /secret option array/); - }) - - describe('when an array', function () { - it('should sign cookies', function (done) { - var server = createServer({ secret: ['keyboard cat', 'nyan cat'] }, function (req, res) { - req.session.user = 'bob'; - res.end(req.session.user); - }); - - request(server) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'bob', done); - }) - - it('should sign cookies with first element', function (done) { - var store = new session.MemoryStore(); - - var server1 = createServer({ secret: ['keyboard cat', 'nyan cat'], store: store }, function (req, res) { - req.session.user = 'bob'; - res.end(req.session.user); - }); - - var server2 = createServer({ secret: 'nyan cat', store: store }, function (req, res) { - res.end(String(req.session.user)); - }); - - request(server1) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'bob', function (err, res) { - if (err) return done(err); - request(server2) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'undefined', done); - }); - }); - - it('should read cookies using all elements', function (done) { - var store = new session.MemoryStore(); - - var server1 = createServer({ secret: 'nyan cat', store: store }, function (req, res) { - req.session.user = 'bob'; - res.end(req.session.user); - }); - - var server2 = createServer({ secret: ['keyboard cat', 'nyan cat'], store: store }, function (req, res) { - res.end(String(req.session.user)); - }); - - request(server1) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, 'bob', function (err, res) { - if (err) return done(err); - request(server2) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'bob', done); - }); - }); - }) - }) - - describe('unset option', function () { - it('should reject unknown values', function(){ - assert.throws(session.bind(null, { unset: 'bogus!' }), /unset.*must/) - }); - - 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(); - }); - - request(app) - .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) - .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) - done(); - }); - }); - }); - }); - }); - - 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(); - }); - - request(app) - .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) - .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) - done(); - }); - }); - }); - }); - }); - - 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(); - }); - - request(app) - .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) - done(); - }); - }); - }); - - it('should pass session destroy error', function (done) { - var cb = after(2, done) - var store = new session.MemoryStore() - var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { - req.session = null - res.end('session destroyed') - }) - - store.destroy = function destroy(sid, callback) { - callback(new Error('boom!')) - } - - server.on('error', function onerror(err) { - assert.ok(err) - assert.equal(err.message, 'boom!') - cb() - }) - - request(server) - .get('/') - .expect(200, 'session destroyed', cb) - }) - }); - - describe('res.end patch', function () { - it('should correctly handle res.end/res.write patched prior', function (done) { - var app = express() - - app.use(writePatch()) - app.use(createSession()) - app.use(function (req, res) { - req.session.hit = true - res.write('hello, ') - res.end('world') - }) - - request(app) - .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) { - req.session.hit = true - res.write('hello, ') - res.end('world') - }) - - request(app) - .get('/') - .expect(200, 'hello, world', done) - }) - }) - - describe('req.session', function(){ - it('should persist', function(done){ - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - req.session.count = req.session.count || 0 - req.session.count++ - res.end('hits: ' + req.session.count) - }) - - request(server) - .get('/') - .expect(200, 'hits: 1', function (err, res) { - if (err) return done(err) - store.load(sid(res), function (err, sess) { - if (err) return done(err) - assert.ok(sess) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'hits: 2', done) - }) - }) - }) - - it('should only set-cookie when modified', function(done){ - var modify = true; - - 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) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, '2', function (err, res) { - if (err) return done(err) - var val = cookie(res); - modify = false; - - request(app) - .get('/') - .set('Cookie', val) - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, '2', function (err, res) { - if (err) return done(err) - modify = true; - - request(app) - .get('/') - .set('Cookie', val) - .expect(shouldSetCookie('connect.sid')) - .expect(200, '3', done) - }); - }); - }); - }) - - 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) throw err; - assert(!req.session, 'req.session after destroy'); - res.end(); - }); - }); - - request(app) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, 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) throw err; - assert.notEqual(id, req.session.id) - res.end(); - }); - }); - - request(app) - .get('/') - .expect(shouldSetCookie('connect.sid')) - .expect(200, function (err, res) { - if (err) return done(err) - var id = sid(res) - request(app) - .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(); - }); - }); - }) - }) - - describe('.reload()', function () { - it('should reload session from store', function (done) { - var server = createServer(null, function (req, res) { - if (req.url === '/') { - req.session.active = true - res.end('session created') - return - } - - req.session.url = req.url - - if (req.url === '/bar') { - res.end('saw ' + req.session.url) - return - } - - request(server) - .get('/bar') - .set('Cookie', val) - .expect(200, 'saw /bar', function (err, resp) { - if (err) return done(err) - req.session.reload(function (err) { - if (err) return done(err) - res.end('saw ' + req.session.url) - }) - }) - }) - var val - - request(server) - .get('/') - .expect(200, 'session created', function (err, res) { - if (err) return done(err) - val = cookie(res) - request(server) - .get('/foo') - .set('Cookie', val) - .expect(200, 'saw /bar', done) - }) - }) - - it('should error is session missing', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - if (req.url === '/') { - req.session.active = true - res.end('session created') - return - } - - store.clear(function (err) { - if (err) return done(err) - req.session.reload(function (err) { - res.statusCode = err ? 500 : 200 - res.end(err ? err.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(500, 'failed to load session', done) - }) - }) - }) - - describe('.save()', function () { - it('should save session to store', function (done) { - var store = new session.MemoryStore() - var server = createServer({ store: store }, function (req, res) { - req.session.hit = true - req.session.save(function (err) { - if (err) return res.end(err.message) - store.get(req.session.id, function (err, sess) { - if (err) return res.end(err.message) - res.end(sess ? 'stored' : 'empty') - }) - }) - }) - - request(server) - .get('/') - .expect(200, 'stored', done) - }) - - 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 - req.session.save(function (err) { - if (err) return res.end(err.message) - res.end('saved') - }) - }) - - var _set = store.set - store.set = function set(sid, sess, callback) { - count++ - _set.call(store, sid, sess, callback) - } - - request(server) - .get('/') - .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() - }) - }) - }) - }) - - describe('.touch()', function () { - it('should reset session expiration', function (done) { - var store = new session.MemoryStore() - var server = createServer({ resave: false, store: store, cookie: { maxAge: min } }, function (req, res) { - req.session.hit = true - req.session.touch() - res.end() - }) - - request(server) - .get('/') - .expect(200, function (err, res) { - if (err) return done(err) - var id = sid(res) - store.get(id, function (err, sess) { - if (err) return done(err) - var exp = new Date(sess.cookie.expires) - setTimeout(function () { - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, function (err, res) { - 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()) - done() - }) - }) - }, 100) - }) - }) - }) - }) - - 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(); - }); - - request(app) - .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(); - }); - }) - - 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 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(); - }); - - request(app) - .get('/admin/foo') - .expect(shouldSetCookie('connect.sid')) - .expect(200, function (err, res) { - if (err) return done(err) - request(app) - .get('/admin') - .set('Cookie', cookie(res)) - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }); - }) - - 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(); - }); - - 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(); - }); - }) - - 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(); - }); - - request(app) - .get('/') - .expect(shouldSetCookieToValue('previous', 'cookieValue')) - .expect(200, done) - }) - }) - - describe('.secure', function(){ - var app - - before(function () { - app = createRequestListener({ secret: 'keyboard cat', cookie: { secure: true } }) - }) - - it('should set cookie when secure', 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 - }) - - server.on('request', app) - - var agent = new https.Agent({ca: cert}) - var createConnection = agent.createConnection - - agent.createConnection = function (options) { - options.servername = 'express-session.local' - return createConnection.call(this, options) - } - - var req = request(server).get('/') - req.agent(agent) - req.expect(shouldSetCookie('connect.sid')) - req.expect(200, done) - }) - - it('should not set-cookie when insecure', function(done){ - var server = http.createServer(app) - - request(server) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }) - }) - - 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(); - }); - - request(app) - .get('/') - .expect(shouldNotHaveHeader('Set-Cookie')) - .expect(200, done) - }) - - 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) - .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) - }) - }) - - 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) { - 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) - .get('/') - .set('Cookie', val) - .expect(200, '2', function (err, res) { - 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(); - }); - }); - - it('should modify cookie when changed to large value', function(done){ - request(app) - .get('/') - .set('Cookie', val) - .expect(200, '3', function (err, res) { - 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(); - }); - }); - }) - - 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) - .get('/') - .end(function(err, res){ - if (err) return done(err) - assert.equal(expires(res), 'Thu, 01 Jan 1970 00:00:00 GMT') - 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) - .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(); - }); - }) - - it('should not reset cookie', function (done) { - 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') - request(server) - .get('/') - .set('Cookie', val) - .expect(200, function (err, res) { - if (err) return done(err); - assert.ok(!cookie(res)); - done(); - }); - }); - }) - - it('should not reset cookie when modified', function (done) { - var server = createServer(null, function (req, res) { - req.session.cookie.expires = null; - req.session.hit = (req.session.hit || 0) + 1; - 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') - request(server) - .get('/') - .set('Cookie', val) - .expect(200, function (err, res) { - if (err) return done(err); - assert.ok(!cookie(res)); - done(); - }); - }); - }) - }) - }) - }) - }) - - describe('synchronous store', function(){ - it('should respond correctly on save', function(done){ - var store = new SyncStore() - var server = createServer({ store: store }, function (req, res) { - req.session.count = req.session.count || 0 - req.session.count++ - res.end('hits: ' + req.session.count) - }) - - request(server) - .get('/') - .expect(200, 'hits: 1', done) - }) - - it('should respond correctly on destroy', function(done){ - var store = new SyncStore() - var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { - req.session.count = req.session.count || 0 - var count = ++req.session.count - if (req.session.count > 1) { - req.session = null - res.write('destroyed\n') - } - res.end('hits: ' + count) - }) - - request(server) - .get('/') - .expect(200, 'hits: 1', function (err, res) { - if (err) return done(err) - request(server) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, 'destroyed\nhits: 2', done) - }) - }) - }) - - describe('cookieParser()', function () { - it('should read from req.cookies', function(done){ - var app = express() - .use(cookieParser()) - .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat' })) - .use(function(req, res, next){ - req.session.count = req.session.count || 0 - req.session.count++ - res.end(req.session.count.toString()) - }) - - request(app) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, '2', done) - }) - }) - - it('should reject unsigned from req.cookies', function(done){ - var app = express() - .use(cookieParser()) - .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat', key: 'sessid' })) - .use(function(req, res, next){ - req.session.count = req.session.count || 0 - req.session.count++ - res.end(req.session.count.toString()) - }) - - request(app) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - request(app) - .get('/') - .set('Cookie', 'sessid=' + sid(res)) - .expect(200, '1', done) - }) - }) - - it('should reject invalid signature from req.cookies', function(done){ - var app = express() - .use(cookieParser()) - .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) - .use(session({ secret: 'keyboard cat', key: 'sessid' })) - .use(function(req, res, next){ - req.session.count = req.session.count || 0 - req.session.count++ - res.end(req.session.count.toString()) - }) - - request(app) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - var val = cookie(res).replace(/...\./, '.') - request(app) - .get('/') - .set('Cookie', val) - .expect(200, '1', done) - }) - }) - - it('should read from req.signedCookies', function(done){ - var app = express() - .use(cookieParser('keyboard cat')) - .use(function(req, res, next){ delete req.headers.cookie; next() }) - .use(session()) - .use(function(req, res, next){ - req.session.count = req.session.count || 0 - req.session.count++ - res.end(req.session.count.toString()) - }) - - request(app) - .get('/') - .expect(200, '1', function (err, res) { - if (err) return done(err) - request(app) - .get('/') - .set('Cookie', cookie(res)) - .expect(200, '2', 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 createRequestListener(opts, fn) { - var _session = createSession(opts) - var respond = fn || end - - return function onRequest(req, res) { - var server = this - - _session(req, res, function (err) { - if (err && !res._header) { - res.statusCode = err.status || 500 - res.end(err.message) - return - } - - if (err) { - server.emit('error', err) - return - } - - respond(req, res) - }) - } -} - -function createSession(opts) { - var options = opts || {} - - if (!('cookie' in options)) { - options.cookie = { maxAge: 60 * 1000 } - } - - if (!('secret' in options)) { - options.secret = 'keyboard cat' - } - - return session(options) -} - -function end(req, res) { - res.end() -} - -function expires(res) { - var match = /Expires=([^;]+)/.exec(cookie(res)); - return match ? match[1] : undefined; -} - -function shouldNotHaveHeader(header) { - return function (res) { - assert.ok(!(header.toLowerCase() in res.headers), 'should not have ' + header + ' header') - } -} - -function shouldSetCookie(name) { - return function (res) { - var header = cookie(res) - assert.ok(header, 'should have a cookie header') - assert.equal(header.split('=')[0], name, 'should set cookie ' + name) - } -} - -function shouldSetCookieToValue(name, val) { - return function (res) { - var header = cookie(res); - 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) - } -} - -function sid(res) { - var match = /^[^=]+=s%3A([^;\.]+)[\.;]/.exec(cookie(res)) - var val = match ? match[1] : undefined - return val -} - -function writePatch() { - var ended = false - return function addWritePatch(req, res, next) { - var _end = res.end - var _write = res.write - - 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) - } - - next() - } -} - -function SyncStore() { - this.sessions = Object.create(null); -} - -SyncStore.prototype.__proto__ = session.Store.prototype; - -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(); -}; + +process.env.NO_DEPRECATION = 'express-session'; + +var after = require('after') +var assert = require('assert') +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 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') + }) + + 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); + + request(app) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + + it('should error without secret', function(done){ + request(createServer({ secret: undefined })) + .get('/') + .expect(500, /secret.*required/, done) + }) + + 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'); + + request(app) + .get('/') + .expect(200, '', done) + }) + + it('should create a new session', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.session.active = true + res.end('session active') + }); + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'session active', function (err, res) { + if (err) return done(err) + store.length(function (err, len) { + if (err) return done(err) + assert.equal(len, 1) + done() + }) + }) + }) + + it('should load session from cookie sid', function (done) { + var count = 0 + var server = createServer(null, 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) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'session 1', done) + }) + }) + + it('should pass session fetch error', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + res.end('hello, world') + }) + + store.get = function destroy(sid, callback) { + callback(new Error('boom!')) + } + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'hello, world', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(500, 'boom!', done) + }) + }) + + it('should treat ENOENT session fetch error as not found', 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) + }) + + store.get = function destroy(sid, callback) { + var err = new Error('boom!') + err.code = 'ENOENT' + callback(err) + } + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'session 1', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'session 2', done) + }) + }) + + it('should create multiple sessions', function (done) { + var cb = after(2, check) + var count = 0 + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + var isnew = req.session.num === undefined + req.session.num = req.session.num || ++count + res.end('session ' + (isnew ? 'created' : 'updated')) + }); + + function check(err) { + if (err) return done(err) + store.all(function (err, sess) { + if (err) return done(err) + assert.equal(Object.keys(sess).length, 2) + done() + }) + } + + request(server) + .get('/') + .expect(200, 'session created', cb) + + request(server) + .get('/') + .expect(200, 'session created', cb) + }) + + 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'); + + request(app) + .get('/') + .expect('Content-Type', 'text/plain') + .expect(200, 'Hello, world!', done); + }) + + it('should handle res.end(null) calls', function (done) { + var server = createServer(null, function (req, res) { + res.end(null) + }) + + request(server) + .get('/') + .expect(200, '', done) + }) + + it('should handle reserved properties in storage', function (done) { + var count = 0 + var sid + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + sid = req.session.id + req.session.num = req.session.num || ++count + res.end('session saved') + }) + + request(server) + .get('/') + .expect(200, 'session saved', function (err, res) { + if (err) return done(err) + store.get(sid, function (err, sess) { + if (err) return done(err) + // save is reserved + sess.save = 'nope' + store.set(sid, sess, function (err) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'session saved', done) + }) + }) + }) + }) + + it('should only have session data enumerable (and cookie)', function (done) { + var server = createServer(null, function (req, res) { + req.session.test1 = 1 + req.session.test2 = 'b' + res.end(Object.keys(req.session).sort().join(',')) + }) + + request(server) + .get('/') + .expect(200, 'cookie,test1,test2', done) + }) + + it('should not save with bogus req.sessionID', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.sessionID = function () {} + req.session.test1 = 1 + req.session.test2 = 'b' + res.end() + }) + + request(server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, function (err) { + if (err) return done(err) + store.length(function (err, length) { + if (err) return done(err) + assert.equal(length, 0) + done() + }) + }) + }) + + 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() + }) + }) + + 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 + res.setHeader('Content-Length', '0') + 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() + }) + }) + + 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 + res.setHeader('Content-Length', '12') + res.write('hello, ') + 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() + }) + }) + + 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 + res.setHeader('Content-Length', '13') + 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() + }) + }) + }) + + describe('when sid not in store', function () { + it('should create a new session', 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.clear(function (err) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'session 2', done) + }) + }) + }) + + it('should have a new sid', 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) + 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() + }) + }) + }) + }) + }) + + describe('when sid not properly signed', function () { + it('should generate new session', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, key: 'sessid' }, function (req, res) { + var isnew = req.session.active === undefined + req.session.active = true + res.end('session ' + (isnew ? 'created' : 'read')) + }) + + request(server) + .get('/') + .expect(shouldSetCookie('sessid')) + .expect(200, 'session created', function (err, res) { + if (err) return done(err) + var val = sid(res) + assert.ok(val) + request(server) + .get('/') + .set('Cookie', 'sessid=' + val) + .expect(shouldSetCookie('sessid')) + .expect(200, 'session created', done) + }) + }) + + it('should not attempt fetch from store', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store, key: 'sessid' }, function (req, res) { + var isnew = req.session.active === undefined + req.session.active = true + res.end('session ' + (isnew ? 'created' : 'read')) + }) + + request(server) + .get('/') + .expect(shouldSetCookie('sessid')) + .expect(200, 'session created', function (err, res) { + if (err) return done(err) + var val = cookie(res).replace(/...\./, '.') + + assert.ok(val) + request(server) + .get('/') + .set('Cookie', val) + .expect(shouldSetCookie('sessid')) + .expect(200, 'session created', done) + }) + }) + }) + + describe('when session expired in store', function () { + it('should create a new session', function (done) { + var count = 0 + var store = new session.MemoryStore() + var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) + setTimeout(function () { + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'session 2', done) + }, 20) + }) + }) + + it('should have a new sid', function (done) { + var count = 0 + var store = new session.MemoryStore() + var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) + 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() + }) + }, 15) + }) + }) + + it('should not exist in store', function (done) { + var count = 0 + var store = new session.MemoryStore() + var server = createServer({ store: store, cookie: { maxAge: 5 } }, 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) + setTimeout(function () { + store.all(function (err, sess) { + if (err) return done(err) + assert.equal(Object.keys(sess).length, 0) + done() + }) + }, 10) + }) + }) + }) + + describe('proxy option', function(){ + describe('when enabled', function(){ + var server + before(function () { + server = createServer({ proxy: true, cookie: { secure: true, maxAge: 5 }}) + }) + + it('should trust X-Forwarded-Proto when string', function(done){ + request(server) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should trust X-Forwarded-Proto when comma-separated list', function(done){ + request(server) + .get('/') + .set('X-Forwarded-Proto', 'https,http') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should work when no header', function(done){ + request(server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + }) + + describe('when disabled', function(){ + var server + before(function () { + server = createServer({ proxy: false, cookie: { secure: true, maxAge: 5 }}) + }) + + it('should not trust X-Forwarded-Proto', function(done){ + request(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) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 'true', done) + }) + }) + + describe('when unspecified', function(){ + var server + before(function () { + server = createServer({ cookie: { secure: true, maxAge: 5 }}) + }) + + it('should not trust X-Forwarded-Proto', function(done){ + request(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) + .get('/') + .set('X-Forwarded-Proto', 'https') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'true', 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) + }); + + 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('key option', function(){ + it('should default to "connect.sid"', function(done){ + request(createServer()) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done) + }) + + it('should allow overriding', function(done){ + request(createServer({ key: 'session_id' })) + .get('/') + .expect(shouldSetCookie('session_id')) + .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){ + var save = req.session.save; + req.session.user = 'bob'; + res.end(); + }); + + 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 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){ + var save = req.session.save; + req.session.user = 'bob'; + res.end(); + }); + + 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) + }); + }); + }); + + 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(); + }); + + request(app) + .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', '2') + .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(); + }); + + request(app) + .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', '2') + .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(); + }); + + request(app) + .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); + }); + }); + + 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(); + }); + + request(app) + .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', '2') + .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') + }) + + store.touch = function touch(sid, sess, callback) { + callback(new Error('boom!')) + } + + server.on('error', function onerror(err) { + assert.ok(err) + assert.equal(err.message, 'boom!') + cb() + }) + + request(server) + .get('/') + .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(); + }); + + request(app) + .get('/') + .expect('x-count', '1') + .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(); + }); + + request(app) + .get('/') + .expect('x-count', '1') + .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(); + }); + + request(app) + .get('/') + .expect('x-count', '0') + .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(); + }); + + request(app) + .get('/') + .expect('x-count', '1') + .expect(shouldSetCookie('connect.sid')) + .expect(200, done); + }); + + it('should pass session save error', function (done) { + var cb = after(2, done) + var store = new session.MemoryStore() + var server = createServer({ store: store, saveUninitialized: true }, function (req, res) { + res.end('session saved') + }) + + store.set = function destroy(sid, sess, callback) { + callback(new Error('boom!')) + } + + server.on('error', function onerror(err) { + assert.ok(err) + assert.equal(err.message, 'boom!') + cb() + }) + + request(server) + .get('/') + .expect(200, 'session saved', cb) + }) + + it('should prevent uninitialized session from being touched', function (done) { + var cb = after(1, done) + var store = new session.MemoryStore() + var server = createServer({ saveUninitialized: false, store: store, cookie: { maxAge: min } }, function (req, res) { + res.end() + }) + + store.touch = function () { + cb(new Error('should not be called')) + } + + request(server) + .get('/') + .expect(200, cb) + }) + }); + + describe('secret option', function () { + it('should reject empty arrays', function () { + assert.throws(createServer.bind(null, { secret: [] }), /secret option array/); + }) + + describe('when an array', function () { + it('should sign cookies', function (done) { + var server = createServer({ secret: ['keyboard cat', 'nyan cat'] }, function (req, res) { + req.session.user = 'bob'; + res.end(req.session.user); + }); + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'bob', done); + }) + + it('should sign cookies with first element', function (done) { + var store = new session.MemoryStore(); + + var server1 = createServer({ secret: ['keyboard cat', 'nyan cat'], store: store }, function (req, res) { + req.session.user = 'bob'; + res.end(req.session.user); + }); + + var server2 = createServer({ secret: 'nyan cat', store: store }, function (req, res) { + res.end(String(req.session.user)); + }); + + request(server1) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'bob', function (err, res) { + if (err) return done(err); + request(server2) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'undefined', done); + }); + }); + + it('should read cookies using all elements', function (done) { + var store = new session.MemoryStore(); + + var server1 = createServer({ secret: 'nyan cat', store: store }, function (req, res) { + req.session.user = 'bob'; + res.end(req.session.user); + }); + + var server2 = createServer({ secret: ['keyboard cat', 'nyan cat'], store: store }, function (req, res) { + res.end(String(req.session.user)); + }); + + request(server1) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, 'bob', function (err, res) { + if (err) return done(err); + request(server2) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'bob', done); + }); + }); + }) + }) + + describe('unset option', function () { + it('should reject unknown values', function(){ + assert.throws(session.bind(null, { unset: 'bogus!' }), /unset.*must/) + }); + + 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(); + }); + + request(app) + .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) + .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) + done(); + }); + }); + }); + }); + }); + + 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(); + }); + + request(app) + .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) + .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) + done(); + }); + }); + }); + }); + }); + + 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(); + }); + + request(app) + .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) + done(); + }); + }); + }); + + it('should pass session destroy error', function (done) { + var cb = after(2, done) + var store = new session.MemoryStore() + var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { + req.session = null + res.end('session destroyed') + }) + + store.destroy = function destroy(sid, callback) { + callback(new Error('boom!')) + } + + server.on('error', function onerror(err) { + assert.ok(err) + assert.equal(err.message, 'boom!') + cb() + }) + + request(server) + .get('/') + .expect(200, 'session destroyed', cb) + }) + }); + + describe('res.end patch', function () { + it('should correctly handle res.end/res.write patched prior', function (done) { + var app = express() + + app.use(writePatch()) + app.use(createSession()) + app.use(function (req, res) { + req.session.hit = true + res.write('hello, ') + res.end('world') + }) + + request(app) + .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) { + req.session.hit = true + res.write('hello, ') + res.end('world') + }) + + request(app) + .get('/') + .expect(200, 'hello, world', done) + }) + }) + + describe('req.session', function(){ + it('should persist', function(done){ + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.session.count = req.session.count || 0 + req.session.count++ + res.end('hits: ' + req.session.count) + }) + + request(server) + .get('/') + .expect(200, 'hits: 1', function (err, res) { + if (err) return done(err) + store.load(sid(res), function (err, sess) { + if (err) return done(err) + assert.ok(sess) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'hits: 2', done) + }) + }) + }) + + it('should only set-cookie when modified', function(done){ + var modify = true; + + 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) + .get('/') + .expect(200, '1', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, '2', function (err, res) { + if (err) return done(err) + var val = cookie(res); + modify = false; + + request(app) + .get('/') + .set('Cookie', val) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, '2', function (err, res) { + if (err) return done(err) + modify = true; + + request(app) + .get('/') + .set('Cookie', val) + .expect(shouldSetCookie('connect.sid')) + .expect(200, '3', done) + }); + }); + }); + }) + + 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) throw err; + assert(!req.session, 'req.session after destroy'); + res.end(); + }); + }); + + request(app) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 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) throw err; + assert.notEqual(id, req.session.id) + res.end(); + }); + }); + + request(app) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, function (err, res) { + if (err) return done(err) + var id = sid(res) + request(app) + .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(); + }); + }); + }) + }) + + describe('.reload()', function () { + it('should reload session from store', function (done) { + var server = createServer(null, function (req, res) { + if (req.url === '/') { + req.session.active = true + res.end('session created') + return + } + + req.session.url = req.url + + if (req.url === '/bar') { + res.end('saw ' + req.session.url) + return + } + + request(server) + .get('/bar') + .set('Cookie', val) + .expect(200, 'saw /bar', function (err, resp) { + if (err) return done(err) + req.session.reload(function (err) { + if (err) return done(err) + res.end('saw ' + req.session.url) + }) + }) + }) + var val + + request(server) + .get('/') + .expect(200, 'session created', function (err, res) { + if (err) return done(err) + val = cookie(res) + request(server) + .get('/foo') + .set('Cookie', val) + .expect(200, 'saw /bar', done) + }) + }) + + it('should error is session missing', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + if (req.url === '/') { + req.session.active = true + res.end('session created') + return + } + + store.clear(function (err) { + if (err) return done(err) + req.session.reload(function (err) { + res.statusCode = err ? 500 : 200 + res.end(err ? err.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(500, 'failed to load session', done) + }) + }) + }) + + describe('.save()', function () { + it('should save session to store', function (done) { + var store = new session.MemoryStore() + var server = createServer({ store: store }, function (req, res) { + req.session.hit = true + req.session.save(function (err) { + if (err) return res.end(err.message) + store.get(req.session.id, function (err, sess) { + if (err) return res.end(err.message) + res.end(sess ? 'stored' : 'empty') + }) + }) + }) + + request(server) + .get('/') + .expect(200, 'stored', done) + }) + + 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 + req.session.save(function (err) { + if (err) return res.end(err.message) + res.end('saved') + }) + }) + + var _set = store.set + store.set = function set(sid, sess, callback) { + count++ + _set.call(store, sid, sess, callback) + } + + request(server) + .get('/') + .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() + }) + }) + }) + }) + + describe('.touch()', function () { + it('should reset session expiration', function (done) { + var store = new session.MemoryStore() + var server = createServer({ resave: false, store: store, cookie: { maxAge: min } }, function (req, res) { + req.session.hit = true + req.session.touch() + res.end() + }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + var id = sid(res) + store.get(id, function (err, sess) { + if (err) return done(err) + var exp = new Date(sess.cookie.expires) + setTimeout(function () { + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, function (err, res) { + 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()) + done() + }) + }) + }, 100) + }) + }) + }) + }) + + 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(); + }); + + request(app) + .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(); + }); + }) + + 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 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(); + }); + + request(app) + .get('/admin/foo') + .expect(shouldSetCookie('connect.sid')) + .expect(200, function (err, res) { + if (err) return done(err) + request(app) + .get('/admin') + .set('Cookie', cookie(res)) + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }); + }) + + 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(); + }); + + 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(); + }); + }) + + 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(); + }); + + request(app) + .get('/') + .expect(shouldSetCookieToValue('previous', 'cookieValue')) + .expect(200, done) + }) + }) + + describe('.secure', function(){ + var app + + before(function () { + app = createRequestListener({ secret: 'keyboard cat', cookie: { secure: true } }) + }) + + it('should set cookie when secure', 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 + }) + + server.on('request', app) + + var agent = new https.Agent({ca: cert}) + var createConnection = agent.createConnection + + agent.createConnection = function (options) { + options.servername = 'express-session.local' + return createConnection.call(this, options) + } + + var req = request(server).get('/') + req.agent(agent) + req.expect(shouldSetCookie('connect.sid')) + req.expect(200, done) + }) + + it('should not set-cookie when insecure', function(done){ + var server = http.createServer(app) + + request(server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + }) + + 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(); + }); + + request(app) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) + }) + + 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) + .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) + }) + }) + + 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) { + 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) + .get('/') + .set('Cookie', val) + .expect(200, '2', function (err, res) { + 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(); + }); + }); + + it('should modify cookie when changed to large value', function(done){ + request(app) + .get('/') + .set('Cookie', val) + .expect(200, '3', function (err, res) { + 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(); + }); + }); + }) + + 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) + .get('/') + .end(function(err, res){ + if (err) return done(err) + assert.equal(expires(res), 'Thu, 01 Jan 1970 00:00:00 GMT') + 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) + .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(); + }); + }) + + it('should not reset cookie', function (done) { + 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') + request(server) + .get('/') + .set('Cookie', val) + .expect(200, function (err, res) { + if (err) return done(err); + assert.ok(!cookie(res)); + done(); + }); + }); + }) + + it('should not reset cookie when modified', function (done) { + var server = createServer(null, function (req, res) { + req.session.cookie.expires = null; + req.session.hit = (req.session.hit || 0) + 1; + 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') + request(server) + .get('/') + .set('Cookie', val) + .expect(200, function (err, res) { + if (err) return done(err); + assert.ok(!cookie(res)); + done(); + }); + }); + }) + }) + }) + }) + }) + + describe('synchronous store', function(){ + it('should respond correctly on save', function(done){ + var store = new SyncStore() + var server = createServer({ store: store }, function (req, res) { + req.session.count = req.session.count || 0 + req.session.count++ + res.end('hits: ' + req.session.count) + }) + + request(server) + .get('/') + .expect(200, 'hits: 1', done) + }) + + it('should respond correctly on destroy', function(done){ + var store = new SyncStore() + var server = createServer({ store: store, unset: 'destroy' }, function (req, res) { + req.session.count = req.session.count || 0 + var count = ++req.session.count + if (req.session.count > 1) { + req.session = null + res.write('destroyed\n') + } + res.end('hits: ' + count) + }) + + request(server) + .get('/') + .expect(200, 'hits: 1', function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'destroyed\nhits: 2', done) + }) + }) + }) + + describe('cookieParser()', function () { + it('should read from req.cookies', function(done){ + var app = express() + .use(cookieParser()) + .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) + .use(session({ secret: 'keyboard cat' })) + .use(function(req, res, next){ + req.session.count = req.session.count || 0 + req.session.count++ + res.end(req.session.count.toString()) + }) + + request(app) + .get('/') + .expect(200, '1', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, '2', done) + }) + }) + + it('should reject unsigned from req.cookies', function(done){ + var app = express() + .use(cookieParser()) + .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) + .use(session({ secret: 'keyboard cat', key: 'sessid' })) + .use(function(req, res, next){ + req.session.count = req.session.count || 0 + req.session.count++ + res.end(req.session.count.toString()) + }) + + request(app) + .get('/') + .expect(200, '1', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', 'sessid=' + sid(res)) + .expect(200, '1', done) + }) + }) + + it('should reject invalid signature from req.cookies', function(done){ + var app = express() + .use(cookieParser()) + .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() }) + .use(session({ secret: 'keyboard cat', key: 'sessid' })) + .use(function(req, res, next){ + req.session.count = req.session.count || 0 + req.session.count++ + res.end(req.session.count.toString()) + }) + + request(app) + .get('/') + .expect(200, '1', function (err, res) { + if (err) return done(err) + var val = cookie(res).replace(/...\./, '.') + request(app) + .get('/') + .set('Cookie', val) + .expect(200, '1', done) + }) + }) + + it('should read from req.signedCookies', function(done){ + var app = express() + .use(cookieParser('keyboard cat')) + .use(function(req, res, next){ delete req.headers.cookie; next() }) + .use(session()) + .use(function(req, res, next){ + req.session.count = req.session.count || 0 + req.session.count++ + res.end(req.session.count.toString()) + }) + + request(app) + .get('/') + .expect(200, '1', function (err, res) { + if (err) return done(err) + request(app) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, '2', 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 createRequestListener(opts, fn) { + var _session = createSession(opts) + var respond = fn || end + + return function onRequest(req, res) { + var server = this + + _session(req, res, function (err) { + if (err && !res._header) { + res.statusCode = err.status || 500 + res.end(err.message) + return + } + + if (err) { + server.emit('error', err) + return + } + + respond(req, res) + }) + } +} + +function createSession(opts) { + var options = opts || {} + + if (!('cookie' in options)) { + options.cookie = { maxAge: 60 * 1000 } + } + + if (!('secret' in options)) { + options.secret = 'keyboard cat' + } + + return session(options) +} + +function end(req, res) { + res.end() +} + +function expires(res) { + var match = /Expires=([^;]+)/.exec(cookie(res)); + return match ? match[1] : undefined; +} + +function shouldNotHaveHeader(header) { + return function (res) { + assert.ok(!(header.toLowerCase() in res.headers), 'should not have ' + header + ' header') + } +} + +function shouldSetCookie(name) { + return function (res) { + var header = cookie(res) + assert.ok(header, 'should have a cookie header') + assert.equal(header.split('=')[0], name, 'should set cookie ' + name) + } +} + +function shouldSetCookieToValue(name, val) { + return function (res) { + var header = cookie(res); + 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) + } +} + +function sid(res) { + var match = /^[^=]+=s%3A([^;\.]+)[\.;]/.exec(cookie(res)) + var val = match ? match[1] : undefined + return val +} + +function writePatch() { + var ended = false + return function addWritePatch(req, res, next) { + var _end = res.end + var _write = res.write + + 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) + } + + next() + } +} + +function SyncStore() { + this.sessions = Object.create(null); +} + +SyncStore.prototype.__proto__ = session.Store.prototype; + +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(); +};