├── .babelrc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── History.md ├── LICENSE ├── Makefile ├── README.md ├── example ├── app.js └── defer_app.js ├── package.json ├── src ├── memory_store.js ├── session.js └── store.js └── test ├── defer.test.js ├── override.test.js ├── rolling.test.js ├── session.test.js ├── store.test.js └── support ├── defer.js ├── override.js ├── rolling.js ├── server.js └── store.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": 6 6 | } 7 | }] 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | coverage.html 17 | dump.rdb 18 | 19 | lib/ 20 | 21 | yarn.lock 22 | package-lock.json 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "7" 6 | script: "make test-travis" 7 | after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Ordered by date of first contribution. 2 | # Auto-generated by 'contributors' on Sun, 26 Jan 2014 07:04:17 GMT. 3 | # https://github.com/xingrz/node-contributors 4 | 5 | - [dead_horse](https://github.com/dead-horse) 6 | - [Ranadeep](https://github.com/ranadeep47) 7 | - Ivan Pusic 8 | - haoxin 9 | - [Yoshua Wuyts](https://github.com/yoshuawuyts) 10 | - Marcus Ekwall 11 | - [Pavel Vlasov](https://github.com/pavelvlasov) 12 | - [Ilya Kantor](https://github.com/iliakan) 13 | - Naman Goel 14 | - tb01923 15 | - [Jackson Tian](https://github.com/JacksonTian) 16 | - [Michael Kleehammer](https://github.com/mkleehammer) 17 | - Ryan Fink 18 | - Travis Collins 19 | - [Michael Milton](https://github.com/TMiguelT) 20 | - Dzenly 21 | - Rozhnov Alexandr 22 | - Felix 23 | - [Jonathan Cremin](https://github.com/kudos) 24 | - [ali-sdk](https://github.com/ali-sdk) 25 | - [XIE Qianyue](https://github.com/xie-qianyue) 26 | - [Greenkeeper](https://github.com/greenkeeperio) 27 | - zhi.zhou 28 | - Ivan Fraixedes 29 | - Evan King 30 | - Yiyu He 31 | - Nate Silva 32 | - [Yang Aobo](https://github.com/yangaobo) 33 | - chenyancheng 34 | - Jared Chapiewsky 35 | - Jake Trent 36 | - [Nicholas Simmons](https://github.com/nsimmons) 37 | - [niftylettuce](https://github.com/niftylettuce) 38 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | **This has moved to Releases, please see .** 2 | 3 | 2.0.0 / 2017-07-11 4 | ================== 5 | 6 | * Refactor to koa 2 - Supports node 6.x and 7.x (#123) 7 | 8 | 1.11.6 / 2017-06-12 9 | ================== 10 | 11 | * fix: cookie options may be changed 12 | 13 | 1.11.5 / 2017-01-16 14 | ================== 15 | 16 | * fix: store use options.maxAge (#112) 17 | 18 | 1.11.4 / 2016-12-01 19 | ================== 20 | 21 | * docs: maxage => maxAge 22 | * fix: maxage => maxAge (#107) 23 | * update default cookie maxage in readme (#84) 24 | * add koa-generic-session-sequelize session store (#103) 25 | 26 | 1.11.3 / 2016-07-24 27 | ================== 28 | 29 | * fix: always refresh session (#95) 30 | 31 | 1.11.2 / 2016-07-12 32 | ================== 33 | 34 | * fix: npm pack should not include test dir (#92) 35 | 36 | 1.11.1 / 2016-06-23 37 | ================== 38 | 39 | * fix: make sure ctx.session exists when cookie.path = '/' (#91) 40 | 41 | 1.11.0 / 2016-06-20 42 | ================== 43 | 44 | * Add support for overriding session save conditions per request (#89) 45 | 46 | 1.10.2 / 2016-03-23 47 | ================== 48 | 49 | * fix: check if session exits 50 | 51 | 1.10.1 / 2015-12-15 52 | ================== 53 | 54 | * feat: ctx.session as a setter/getter 55 | 56 | 1.10.0 / 2015-11-15 57 | ================== 58 | 59 | * feat: support valid/beforeSave hooks 60 | 61 | 1.9.2 / 2015-08-26 62 | ================== 63 | 64 | * fix: support custom cookie options 65 | 66 | 1.9.1 / 2015-08-23 67 | ================== 68 | 69 | * Fix typo of 'available' 70 | 71 | 1.9.0 / 2015-06-05 72 | ================== 73 | 74 | * Added session id storage option to override session id store logic. 75 | 76 | 1.8.0 / 2015-03-15 77 | ================== 78 | 79 | * feat: support reconnectTimeout 80 | 81 | 1.7.0 / 2015-03-01 82 | ================== 83 | 84 | * Add support for regenerating sessions. 85 | 86 | 1.6.0 / 2015-01-16 87 | ================== 88 | 89 | * Merge pull request #39 from rfink/sess_id_optional_check 90 | * Allow a forced session id that is not available in the cookie 91 | 92 | 1.5.0 / 2015-01-07 93 | ================== 94 | 95 | * Merge pull request #37 from rfink/session_id_context 96 | * Bind genSid to context, add tests 97 | 98 | 1.4.0 / 2015-01-06 99 | ================== 100 | 101 | * fix(sessionId): ensure session id generate before yield next 102 | * fix indent 103 | 104 | 1.3.0 / 2014-11-22 105 | ================== 106 | 107 | * support options.errorHanlder, default throw set error 108 | 109 | 1.2.3 / 2014-11-17 110 | ================== 111 | 112 | * Delete session-id cookie if session not loaded. 113 | * export the session store object 114 | 115 | 1.2.2 / 2014-09-16 116 | ================== 117 | 118 | * use crc instead of buffer-crc32 119 | 120 | 1.2.1 / 2014-08-11 121 | ================== 122 | 123 | * use parseurl get the original url pathname 124 | 125 | 1.2.0 / 2014-08-09 126 | ================== 127 | 128 | * Merge pull request #25 from mekwall/patch-2 129 | * Added description for allowEmpty option 130 | * Added option to allow generation of empty session 131 | 132 | 1.1.3 / 2014-08-05 133 | ================== 134 | 135 | * Merge pull request #23 from mekwall/patch-1 136 | * Fixes MemoryStore not being exported 137 | 138 | 1.1.2 / 2014-07-07 139 | ================== 140 | 141 | * delete cookie.maxAge 142 | 143 | 1.1.1 / 2014-07-07 144 | ================== 145 | 146 | * fix maxage compat 147 | 148 | 1.1.0 / 2014-07-07 149 | ================== 150 | 151 | * seperate ttl and cookie.maxage 152 | 153 | 1.0.0 / 2014-06-29 154 | ================== 155 | 156 | * refactor 157 | * rename to koa-generic-session 158 | 159 | 0.4.1 / 2014-06-29 160 | ================== 161 | 162 | * warn rename to koa-generic-session 163 | 164 | 0.4.0 / 2014-06-18 165 | ================== 166 | 167 | * use uid-safe to generate default sid 168 | * maxAge -> maxage, close #18 169 | * Merge pull request #17 from yoshuawuyts/patch-1 170 | * Update README.md 171 | 172 | 0.3.3 / 2014-05-23 173 | ================== 174 | 175 | * bump dependencies 176 | 177 | 0.3.2 / 2014-05-22 178 | ================== 179 | 180 | * support options.genSid, fixed #16 181 | 182 | 0.3.1 / 2014-03-27 183 | ================== 184 | 185 | * fix store options bug, fixed #13 186 | 187 | 0.3.0 / 2014-03-15 188 | ================== 189 | 190 | * Merge pull request #11 from dead-horse/issue9-rolling-session 191 | * fix readme 192 | * add rolling session options, fixed #9 193 | * Merge pull request #10 from dead-horse/issue8-refresh 194 | * fixed #8, only gen cookie and set session when session is really modified 195 | 196 | 0.2.1 / 2014-03-14 197 | ================== 198 | 199 | * fix path error, fix defer session setter 200 | 201 | 0.2.0 / 2014-03-14 202 | ================== 203 | 204 | * Merge pull request #7 from dead-horse/issue6-defer 205 | * remove coverage 206 | * update readme 207 | * fix travis 208 | * finish defer, fixed #6 209 | * get session return status 210 | * add test for defer 211 | * add defer session 212 | 213 | 0.1.0 / 2014-02-27 214 | ================== 215 | 216 | * fix cookie.expires 217 | 218 | 0.1.0-beta1 / 2014-02-27 219 | ================== 220 | 221 | * update readme 222 | * seperate Store 223 | 224 | 0.0.9 / 2014-02-27 225 | ================== 226 | 227 | * fix new session setCookie twice 228 | 229 | 0.0.8 / 2014-02-11 230 | ================== 231 | 232 | * Merge pull request #3 from dead-horse/fix-cookie 233 | * get session error, throw it 234 | * only set the cookie when sessin was modified 235 | * Changed position of cookies.set 236 | * fix test 237 | * fix typo 238 | 239 | 0.0.7 / 2014-02-01 240 | ================== 241 | 242 | * fix maxage and compat connect type maxAge 243 | * add autod 244 | 245 | 0.0.6 / 2014-01-20 246 | ================== 247 | 248 | * when store is disconnect, throw 500 249 | 250 | 0.0.5 / 2013-12-30 251 | ================== 252 | 253 | * add middware name session 254 | 255 | 0.0.4 / 2013-12-29 256 | ================== 257 | 258 | * fix package.json 259 | 260 | 0.0.3 / 2013-12-29 261 | ================== 262 | 263 | * update readme 264 | * add cookie in session 265 | 266 | 0.0.2 / 2013-12-28 267 | ================== 268 | 269 | * Release 0.0.2 270 | * update readme 271 | * remove secret 272 | * use default cookie's signature 273 | 274 | 0.0.1 / 2013-12-28 275 | ================== 276 | 277 | * update test 278 | * add travis yml 279 | * update test 280 | * init session with memory store 281 | * Initial commit 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 dead_horse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = tap 3 | TIMEOUT = 3000 4 | MOCHA_OPTS = 5 | 6 | install: 7 | @npm install 8 | 9 | build: 10 | @NODE_ENV=production ./node_modules/babel-cli/bin/babel.js -d lib/ src/ 11 | 12 | test: 13 | make build; 14 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \ 15 | --reporter $(REPORTER) \ 16 | --timeout $(TIMEOUT) \ 17 | --require should \ 18 | --require babel-core/register \ 19 | $(MOCHA_OPTS) \ 20 | $(TESTS) 21 | 22 | 23 | test-cov: 24 | make build; 25 | @NODE_ENV=test node \ 26 | node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha \ 27 | -- -u exports \ 28 | --reporter $(REPORTER) \ 29 | --timeout $(TIMEOUT) \ 30 | --require should \ 31 | --require babel-core/register \ 32 | $(MOCHA_OPTS) \ 33 | $(TESTS) 34 | 35 | test-travis: 36 | make build; 37 | @NODE_ENV=test node \ 38 | node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha \ 39 | --report lcovonly \ 40 | -- -u exports \ 41 | --reporter $(REPORTER) \ 42 | --timeout $(TIMEOUT) \ 43 | --require should \ 44 | --require babel-core/register \ 45 | $(MOCHA_OPTS) \ 46 | $(TESTS) 47 | 48 | autod: 49 | @./node_modules/.bin/autod -w -e example --prefix=~ --keep=supertest,debug, --semver=koa@1 50 | @$(MAKE) install 51 | 52 | .PHONY: test 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | generic-session 2 | ========= 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | [![Coveralls][coveralls-image]][coveralls-url] 7 | [![David deps][david-image]][david-url] 8 | [![node version][node-image]][node-url] 9 | [![npm download][download-image]][download-url] 10 | [![Gittip][gittip-image]][gittip-url] 11 | 12 | [npm-image]: https://img.shields.io/npm/v/koa-generic-session.svg?style=flat-square 13 | [npm-url]: https://npmjs.org/package/koa-generic-session 14 | [travis-image]: https://img.shields.io/travis/koajs/generic-session.svg?style=flat-square 15 | [travis-url]: https://travis-ci.org/koajs/generic-session 16 | [coveralls-image]: https://img.shields.io/coveralls/koajs/generic-session.svg?style=flat-square 17 | [coveralls-url]: https://coveralls.io/r/koajs/generic-session?branch=master 18 | [david-image]: https://img.shields.io/david/koajs/generic-session.svg?style=flat-square 19 | [david-url]: https://david-dm.org/koajs/generic-session 20 | [node-image]: https://img.shields.io/badge/node.js-%3E=_6.0.0-red.svg?style=flat-square 21 | [node-url]: http://nodejs.org/download/ 22 | [download-image]: https://img.shields.io/npm/dm/koa-generic-session.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/koa-generic-session 24 | [gittip-image]: https://img.shields.io/gittip/dead-horse.svg?style=flat-square 25 | [gittip-url]: https://www.gittip.com/dead-horse/ 26 | 27 | Generic session middleware for koa, easy use with custom stores such as [redis](https://github.com/koajs/koa-redis) or [mongo](https://github.com/freakycue/koa-generic-session-mongo), supports defer session getter. 28 | 29 | This middleware will only set a cookie when a session is manually set. Each time the session is modified (and only when the session is modified), it will reset the cookie and session. 30 | 31 | You can use the rolling sessions that will reset the cookie and session for every request which touch the session. Save behavior can be overridden per request. 32 | 33 | **For async/await and Node v6.9.0+ support use v2.x of this package, for older use v1.x** 34 | 35 | ## Usage 36 | 37 | ### Example 38 | 39 | ```js 40 | 41 | var session = require('koa-generic-session'); 42 | var redisStore = require('koa-redis'); 43 | var koa = require('koa'); 44 | 45 | var app = new koa(); // for koa v1 use `var app = koa();` 46 | app.keys = ['keys', 'keykeys']; 47 | app.use(session({ 48 | store: redisStore() 49 | })); 50 | 51 | app.use(function *() { 52 | switch (this.path) { 53 | case '/get': 54 | get.call(this); 55 | break; 56 | case '/remove': 57 | remove.call(this); 58 | break; 59 | case '/regenerate': 60 | yield regenerate.call(this); 61 | break; 62 | } 63 | }); 64 | 65 | function get() { 66 | var session = this.session; 67 | session.count = session.count || 0; 68 | session.count++; 69 | this.body = session.count; 70 | } 71 | 72 | function remove() { 73 | this.session = null; 74 | this.body = 0; 75 | } 76 | 77 | function *regenerate() { 78 | get.call(this); 79 | yield this.regenerateSession(); 80 | get.call(this); 81 | } 82 | 83 | app.listen(8080); 84 | ``` 85 | 86 | * After adding session middleware, you can use `this.session` to set or get the sessions. 87 | * Getting session ID via `this.sessionId`. 88 | * Setting `this.session = null;` will destroy this session. 89 | * Altering `this.session.cookie` changes the cookie options of this user. Also you can use the cookie options in session the store. Use for example `cookie.maxAge` as the session store's ttl. 90 | * Calling `this.regenerateSession` will destroy any existing session and generate a new, empty one in its place. The new session will have a different ID. 91 | * Calling `this.saveSession` will save an existing session (this method was added for [koa-redirect-loop](https://github.com/niftylettuce/koa-redirect-loop)) 92 | * Setting `this.sessionSave = true` will force saving the session regardless of any other options or conditions. 93 | * Setting `this.sessionSave = false` will prevent saving the session regardless of any other options or conditions. 94 | 95 | ### Options 96 | 97 | * `key`: cookie name defaulting to `koa.sid`. 98 | * `prefix`: session prefix for store, defaulting to `koa:sess:`. 99 | * `ttl`: ttl is for sessionStore's expiration time (not to be confused with cookie expiration which is controlled by `cookie.maxAge`), can be a number or a function that returns a number (for dynamic TTL), default to null (means get ttl from `cookie.maxAge` or `cookie.expires`). 100 | * `rolling`: rolling session, always reset the cookie and sessions, defaults to `false`. 101 | * `genSid`: default sid was generated by [uid2](https://github.com/coreh/uid2), you can pass a function to replace it (supports promises/async functions). 102 | * `defer`: defers get session, only generate a session when you use it through `var session = yield this.session;`, defaults to `false`. 103 | * `allowEmpty`: allow generation of empty sessions. 104 | * `errorHandler(err, type, ctx)`: `Store.get` and `Store.set` will throw in some situation, use `errorHandle` to handle these errors by yourself. Default will throw. 105 | * `reconnectTimeout`: When store is disconnected, don't throw `store unavailable` error immediately, wait `reconnectTimeout` to reconnect, default is `10s`. 106 | * `sessionIdStore`: object with get, set, reset methods for passing session id throw requests. 107 | * `valid`: valid(ctx, session), valid session value before use it. 108 | * `beforeSave`: beforeSave(ctx, session), hook before save session. 109 | * `store`: session store instance. It can be any Object that has the methods `set`, `get`, `destroy` like [MemoryStore](https://github.com/koajs/koa-session/blob/master/lib/store.js). 110 | * `cookie`: session cookie settings, defaulting to: 111 | ```js 112 | { 113 | path: '/', 114 | httpOnly: true, 115 | maxAge: 24 * 60 * 60 * 1000 //one day in ms, 116 | overwrite: true, 117 | signed: true 118 | } 119 | ``` 120 | 121 | For a full list of cookie options see [expressjs/cookies](https://github.com/expressjs/cookies#cookiesset-name--value---options--). 122 | 123 | if you set`cookie.maxAge` to `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. 124 | 125 | Notice that `ttl` is different from `cookie.maxAge`, `ttl` set the expire time of sessionStore. So if you set `cookie.maxAge = null`, and `ttl=ms('1d')`, the session will expired after one day, but the cookie will destroy when the user closes the browser. 126 | And mostly you can just ignore `options.ttl`, `koa-generic-session` will parse `cookie.maxAge` as the tll. 127 | 128 | If your application requires dynamic expiration, control `cookie.maxAge` using `ctx.session.cookie.maxAge = dynamicMaxAge`, when you need `ttl` to differ from `cookie.maxAge` (a common example is browser-session cookies having `cookie.maxAge = null`, but you want them to not live indefinitely in the store) specify a function for `ttl`: 129 | 130 | ```js 131 | { 132 | ttl: (session) => { 133 | // Expire browser-session cookies from the store after 1 day 134 | if (session.cookie?.maxAge === null) { 135 | return 1000 * 60 * 60 * 24; 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | ## Hooks 142 | 143 | - `valid()`: valid session value before use it 144 | - `beforeSave()`: hook before save session 145 | 146 | ## Session Store 147 | 148 | You can use any other store to replace the default MemoryStore, it just needs to follow this api: 149 | 150 | * `get(sid)`: get session object by sid 151 | * `set(sid, sess, ttl)`: set session object for sid, with a ttl (in ms) 152 | * `destroy(sid)`: destroy session for sid 153 | 154 | the api needs to return a Promise, Thunk or generator. 155 | 156 | And use these events to report the store's status. 157 | 158 | * `connect` 159 | * `disconnect` 160 | 161 | ### Stores Presented 162 | 163 | - [koa-redis](https://github.com/koajs/koa-redis) to store your session data with redis. 164 | - [koa-mysql-session](https://github.com/tb01923/koa-mysql-session) to store your session data with MySQL. 165 | - [koa-generic-session-mongo](https://github.com/freakycue/koa-generic-session-mongo) to store your session data with MongoDB. 166 | - [koa-pg-session](https://github.com/TMiguelT/koa-pg-session) to store your session data with PostgreSQL. 167 | - [koa-generic-session-rethinkdb](https://github.com/KualiCo/koa-generic-session-rethinkdb) to store your session data with ReThinkDB. 168 | - [koa-sqlite3-session](https://github.com/chichou/koa-sqlite3-session) to store your session data with SQLite3. 169 | - [koa-generic-session-sequelize](https://github.com/natesilva/koa-generic-session-sequelize) to store your session data with the [Sequelize](http://docs.sequelizejs.com/) ORM. 170 | - [koa-generic-session-knex](https://github.com/EarthlingInteractive/koa-generic-session-knex) to store your session data with the [Knex](http://knexjs.org/) query builder. 171 | 172 | ## Graceful shutdown 173 | 174 | Since this middleware comes with an auto-reconnect feature, it's very likely you can't gracefully shutdown after 175 | closing the client as generic-session will try to recover the connection, in those cases you can disable reconnect feature (https://github.com/koajs/generic-session/blob/49b45612877d1a1b8a42dc61bfeba46b71f9cb52/src/session.js#L103-L112) desactivating the client emitter (do this only when stopping the server) 176 | 177 | Example with ioredis 178 | ```js 179 | // ...disconnecting from db 180 | redisClient.emit = () => true; 181 | await redisClient.quit(); 182 | // ...stopping the server 183 | ``` 184 | 185 | ## Licences 186 | (The MIT License) 187 | 188 | Copyright (c) 2013 - 2016 dead-horse and other contributors 189 | 190 | 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: 191 | 192 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 193 | 194 | 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. 195 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | var session = require('..'); 4 | var RedisStore = require('koa-redis'); 5 | 6 | var app = new koa(); 7 | app.keys = ['keys', 'keykeys']; 8 | app.use(session({ 9 | store: new RedisStore() 10 | })); 11 | 12 | app.use(ctx => { 13 | switch (ctx.path) { 14 | case '/get': 15 | get(ctx); 16 | break; 17 | case '/remove': 18 | remove(ctx); 19 | break; 20 | } 21 | }); 22 | 23 | function get() { 24 | var session = this.session; 25 | session.count = session.count || 0; 26 | session.count++; 27 | this.body = session.count; 28 | } 29 | 30 | function remove() { 31 | this.session = null; 32 | this.body = 0; 33 | } 34 | 35 | app.listen(8080); 36 | -------------------------------------------------------------------------------- /example/defer_app.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | var session = require('..'); 4 | var RedisStore = require('koa-redis'); 5 | 6 | var app = new koa(); 7 | app.keys = ['keys', 'keykeys']; 8 | app.use(session({ 9 | defer: true, 10 | store: new RedisStore() 11 | })); 12 | 13 | app.use(ctx => { 14 | switch (this.path) { 15 | case '/get': 16 | await get(ctx); 17 | break; 18 | case '/remove': 19 | remove(ctx); 20 | break; 21 | } 22 | }); 23 | 24 | function* get() { 25 | var session = yield this.session; 26 | session.count = session.count || 0; 27 | session.count++; 28 | this.body = session.count; 29 | } 30 | 31 | function remove() { 32 | this.session = null; 33 | this.body = 0; 34 | } 35 | 36 | app.listen(8080); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-generic-session", 3 | "description": "koa generic session store by memory, redis or others", 4 | "repository": "koajs/generic-session", 5 | "version": "2.3.1", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">= 6.0.0" 9 | }, 10 | "main": "lib/session", 11 | "files": [ 12 | "lib" 13 | ], 14 | "keywords": [ 15 | "koa", 16 | "middleware", 17 | "session" 18 | ], 19 | "author": "dead_horse ", 20 | "dependencies": { 21 | "copy-to": "~2.0.1", 22 | "crc": "~3.5.0", 23 | "debug": "~3.1.0", 24 | "parseurl": "~1.3.1", 25 | "uid-safe": "~2.1.4" 26 | }, 27 | "devDependencies": { 28 | "autod": "~2.8.0", 29 | "babel-cli": "^6.24.1", 30 | "babel-preset-env": "^1.4.0", 31 | "blanket": "*", 32 | "contributors": "*", 33 | "istanbul": "^0.4.5", 34 | "koa": "^2.2.0", 35 | "koa-redis": "~2.1.1", 36 | "mm": "^2.1.0", 37 | "mocha": "^3.2.0", 38 | "pedding": "^1.0.0", 39 | "should": "^11.2.1", 40 | "supertest": "^3.0.0" 41 | }, 42 | "scripts": { 43 | "prepublish": "make build", 44 | "test": "make test" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/memory_store.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - lib/memory_store.js 3 | * Copyright(c) 2014 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 'use strict' 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | 16 | const debug = require('debug')('koa-generic-session:memory_store') 17 | 18 | class MemoryStore { 19 | constructor() { 20 | this.sessions = {} 21 | } 22 | 23 | get(sid) { 24 | debug('get value %j with key %s', this.sessions[sid], sid) 25 | return this.sessions[sid] 26 | } 27 | 28 | set(sid, val) { 29 | debug('set value %j for key %s', val, sid) 30 | this.sessions[sid] = val 31 | } 32 | 33 | destroy(sid) { 34 | delete this.sessions[sid] 35 | } 36 | } 37 | 38 | module.exports = MemoryStore 39 | -------------------------------------------------------------------------------- /src/session.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - lib/session.js 3 | * Copyright(c) 2013 - 2014 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | 16 | const debug = require('debug')('koa-generic-session:session') 17 | const MemoryStore = require('./memory_store') 18 | const crc32 = require('crc').crc32 19 | const parse = require('parseurl') 20 | const Store = require('./store') 21 | const copy = require('copy-to') 22 | const uid = require('uid-safe') 23 | 24 | /** 25 | * Warning message for `MemoryStore` usage in production. 26 | */ 27 | 28 | const warning = 'Warning: koa-generic-session\'s MemoryStore is not\n' + 29 | 'designed for a production environment, as it will leak\n' + 30 | 'memory, and will not scale past a single process.' 31 | 32 | const defaultCookie = { 33 | httpOnly: true, 34 | path: '/', 35 | overwrite: true, 36 | signed: true, 37 | maxAge: 24 * 60 * 60 * 1000 //one day in ms 38 | } 39 | 40 | /** 41 | * setup session store with the given `options` 42 | * @param {Object} options 43 | * - [`key`] cookie name, defaulting to `koa.sid` 44 | * - [`store`] session store instance, default to MemoryStore 45 | * - [`ttl`] store ttl in `ms`, default to cookie maxAge/expires 46 | * - [`prefix`] session prefix for store, defaulting to `koa:sess:` 47 | * - [`cookie`] session cookie settings, defaulting to 48 | * {path: '/', httpOnly: true, maxAge: null, overwrite: true, signed: true} 49 | * - [`defer`] defer get session, 50 | * - [`rolling`] rolling session, always reset the cookie and sessions, default is false 51 | * you should `await ctx.session` to get the session if defer is true, default is false 52 | * - [`genSid`] you can use your own generator for sid (supports promises/async functions) 53 | * - [`errorHandler`] handler for session store get or set error 54 | * - [`valid`] valid(ctx, session), valid session value before use it 55 | * - [`beforeSave`] beforeSave(ctx, session), hook before save session 56 | * - [`sessionIdStore`] object with get, set, reset methods for passing session id throw requests. 57 | */ 58 | 59 | module.exports = function(options = {}) { 60 | 61 | const key = options.key || 'koa.sid' 62 | const client = options.store || new MemoryStore() 63 | const errorHandler = options.errorHandler || defaultErrorHandler 64 | const reconnectTimeout = options.reconnectTimeout || 10000 65 | 66 | const store = new Store(client, { 67 | ttl: options.ttl, 68 | prefix: options.prefix 69 | }) 70 | 71 | const genSid = options.genSid || uid; 72 | const valid = options.valid || noop 73 | const beforeSave = options.beforeSave || noop 74 | 75 | const cookie = Object.assign({}, options.cookie) 76 | copy(defaultCookie).to(cookie) 77 | 78 | let storeStatus = 'available' 79 | let waitStore = Promise.resolve() 80 | 81 | // notify user that this store is not 82 | // meant for a production environment 83 | if ('production' === process.env.NODE_ENV && client instanceof MemoryStore) { 84 | // eslint-disable-next-line 85 | console.warn(warning) 86 | } 87 | 88 | const sessionIdStore = options.sessionIdStore || { 89 | 90 | get: function() { 91 | return this.cookies.get(key, cookie) 92 | }, 93 | 94 | set: function(sid, session) { 95 | if (!this.headerSent || this.writeable) 96 | this.cookies.set(key, sid, session.cookie) 97 | }, 98 | 99 | reset: function() { 100 | if (!this.headerSent || this.writeable) 101 | this.cookies.set(key, null, { expires: new Date(0) }) 102 | } 103 | } 104 | 105 | store.on('disconnect', () => { 106 | if (storeStatus !== 'available') return 107 | storeStatus = 'pending' 108 | waitStore = new Promise((resolve, reject) => { 109 | setTimeout(() => { 110 | if (storeStatus === 'pending') storeStatus = 'unavailable' 111 | reject(new Error('session store is unavailable')) 112 | }, reconnectTimeout) 113 | store.once('connect', resolve) 114 | }) 115 | 116 | }) 117 | 118 | store.on('connect', () => { 119 | storeStatus = 'available' 120 | waitStore = Promise.resolve() 121 | }) 122 | 123 | // save empty session hash for compare 124 | const EMPTY_SESSION_HASH = hash(generateSession()) 125 | 126 | return options.defer ? deferSession : session 127 | 128 | function addCommonAPI(ctx) { 129 | 130 | ctx._sessionSave = null 131 | 132 | // more flexible 133 | Object.defineProperty(ctx, 'sessionSave', { 134 | get: () => { 135 | return ctx._sessionSave 136 | }, 137 | set: (save) => { 138 | ctx._sessionSave = save 139 | } 140 | }) 141 | 142 | } 143 | 144 | /** 145 | * generate a new session 146 | */ 147 | function generateSession() { 148 | const session = {} 149 | //you can alter the cookie options in nexts 150 | session.cookie = {} 151 | for (const prop in cookie) { 152 | session.cookie[prop] = cookie[prop] 153 | } 154 | compatMaxage(session.cookie) 155 | return session 156 | } 157 | 158 | /** 159 | * check url match cookie's path 160 | */ 161 | function matchPath(ctx) { 162 | const pathname = parse(ctx).pathname 163 | const cookiePath = cookie.path || '/' 164 | if (cookiePath === '/') { 165 | return true 166 | } 167 | if (pathname.indexOf(cookiePath) !== 0) { 168 | debug('cookie path not match') 169 | return false 170 | } 171 | return true 172 | } 173 | 174 | /** 175 | * get session from store 176 | * get sessionId from cookie 177 | * save sessionId into context 178 | * get session from store 179 | */ 180 | async function getSession(ctx) { 181 | if (!matchPath(ctx)) return 182 | if (storeStatus === 'pending') { 183 | debug('store is disconnect and pending') 184 | await waitStore 185 | } else if (storeStatus === 'unavailable') { 186 | debug('store is unavailable') 187 | throw new Error('session store is unavailable') 188 | } 189 | 190 | if (!ctx.sessionId) { 191 | ctx.sessionId = sessionIdStore.get.call(ctx) 192 | } 193 | 194 | let session 195 | let isNew = false 196 | if (!ctx.sessionId) { 197 | debug('session id not exist, generate a new one') 198 | session = generateSession() 199 | ctx.sessionId = await genSid.call(ctx, 24) 200 | isNew = true 201 | } else { 202 | try { 203 | session = await store.get(ctx.sessionId) 204 | debug('get session %j with key %s', session, ctx.sessionId) 205 | } catch (err) { 206 | if (err.code === 'ENOENT') { 207 | debug('get session error, code = ENOENT') 208 | } else { 209 | debug('get session error: ', err && err.message) 210 | errorHandler(err, 'get', ctx) 211 | } 212 | } 213 | } 214 | 215 | // make sure the session is still valid 216 | if (!session || 217 | !valid(ctx, session)) { 218 | debug('session is empty or invalid') 219 | session = generateSession() 220 | ctx.sessionId = await genSid.call(ctx, 24) 221 | sessionIdStore.reset.call(ctx) 222 | isNew = true 223 | } 224 | 225 | // get the originHash 226 | const originalHash = !isNew && hash(session) 227 | 228 | return { 229 | originalHash: originalHash, 230 | session: session, 231 | isNew: isNew 232 | } 233 | } 234 | 235 | /** 236 | * after everything done, refresh the session 237 | * if session === null; delete it from store 238 | * if session is modified, update cookie and store 239 | */ 240 | async function refreshSession (ctx, session, originalHash, isNew) { 241 | 242 | // reject any session changes, and do not update session expiry 243 | if(ctx._sessionSave === false) { 244 | return debug('session save disabled') 245 | } 246 | 247 | //delete session 248 | if (!session) { 249 | if (!isNew) { 250 | debug('session set to null, destroy session: %s', ctx.sessionId) 251 | sessionIdStore.reset.call(ctx) 252 | return store.destroy(ctx.sessionId) 253 | } 254 | return debug('a new session and set to null, ignore destroy') 255 | } 256 | 257 | // force saving non-empty session 258 | if(ctx._sessionSave === true) { 259 | debug('session save forced') 260 | return saveNow(ctx, ctx.sessionId, session) 261 | } 262 | 263 | const newHash = hash(session) 264 | // if new session and not modified, just ignore 265 | if (!options.allowEmpty && isNew && newHash === EMPTY_SESSION_HASH) { 266 | return debug('new session and do not modified') 267 | } 268 | 269 | // rolling session will always reset cookie and session 270 | if (!options.rolling && newHash === originalHash) { 271 | return debug('session not modified') 272 | } 273 | 274 | debug('session modified') 275 | 276 | await saveNow(ctx, ctx.sessionId, session) 277 | 278 | } 279 | 280 | async function saveNow(ctx, id, session) { 281 | compatMaxage(session.cookie) 282 | 283 | // custom before save hook 284 | beforeSave(ctx, session) 285 | 286 | //update session 287 | try { 288 | await store.set(id, session) 289 | sessionIdStore.set.call(ctx, id, session) 290 | debug('saved') 291 | } catch (err) { 292 | debug('set session error: ', err && err.message) 293 | errorHandler(err, 'set', ctx) 294 | } 295 | } 296 | 297 | /** 298 | * common session middleware 299 | * each request will generate a new session 300 | * 301 | * ``` 302 | * let session = this.session 303 | * ``` 304 | */ 305 | async function session(ctx, next) { 306 | ctx.sessionStore = store 307 | if (ctx.session || ctx._session) { 308 | return next() 309 | } 310 | const result = await getSession(ctx) 311 | if (!result) { 312 | return next() 313 | } 314 | 315 | addCommonAPI(ctx) 316 | 317 | ctx._session = result.session 318 | 319 | // more flexible 320 | Object.defineProperty(ctx, 'session', { 321 | get() { 322 | return this._session 323 | }, 324 | set(sess) { 325 | this._session = sess 326 | } 327 | }) 328 | 329 | ctx.saveSession = async function saveSession() { 330 | const result = await getSession(ctx) 331 | if (!result) { 332 | return next() 333 | } 334 | return refreshSession(ctx, ctx.session, result.originalHash, result.isNew) 335 | } 336 | 337 | ctx.regenerateSession = async function regenerateSession() { 338 | debug('regenerating session') 339 | if (!result.isNew) { 340 | // destroy the old session 341 | debug('destroying previous session') 342 | await store.destroy(ctx.sessionId) 343 | } 344 | 345 | ctx.session = generateSession() 346 | ctx.sessionId = await genSid.call(ctx, 24) 347 | sessionIdStore.reset.call(ctx) 348 | 349 | debug('created new session: %s', ctx.sessionId) 350 | result.isNew = true 351 | } 352 | 353 | // make sure `refreshSession` always called 354 | let firstError = null 355 | try { 356 | await next() 357 | } catch (err) { 358 | debug('next logic error: %s', err && err.message) 359 | firstError = err 360 | } 361 | // can't use finally because `refreshSession` is async 362 | try { 363 | await refreshSession(ctx, ctx.session, result.originalHash, result.isNew) 364 | } catch (err) { 365 | debug('refresh session error: %s', err && err.message) 366 | if (firstError) ctx.app.emit('error', err, ctx) 367 | firstError = firstError || err 368 | } 369 | if (firstError) throw firstError 370 | } 371 | 372 | /** 373 | * defer session middleware 374 | * only generate and get session when request use session 375 | * 376 | * ``` 377 | * let session = yield this.session 378 | * ``` 379 | */ 380 | async function deferSession(ctx, next) { 381 | ctx.sessionStore = store 382 | 383 | // TODO: 384 | // Accessing ctx.session when it's defined is causing problems 385 | // because it has side effect. So, here we use a flag to determine 386 | // that session property is already defined. 387 | if (ctx.__isSessionDefined) { 388 | return next() 389 | } 390 | let isNew = false 391 | let originalHash = null 392 | let touchSession = false 393 | let getter = false 394 | 395 | // if path not match 396 | if (!matchPath(ctx)) { 397 | return next() 398 | } 399 | 400 | addCommonAPI(ctx) 401 | 402 | Object.defineProperty(ctx, 'session', { 403 | async get() { 404 | if (touchSession) { 405 | return this._session 406 | } 407 | touchSession = true 408 | getter = true 409 | 410 | const result = await getSession(this) 411 | // if cookie path not match 412 | // this route's controller should never use session 413 | if (!result) return 414 | 415 | originalHash = result.originalHash 416 | isNew = result.isNew 417 | this._session = result.session 418 | return this._session 419 | }, 420 | set(value) { 421 | touchSession = true 422 | this._session = value 423 | } 424 | }) 425 | 426 | // internal flag to determine that session is already defined 427 | ctx.__isSessionDefined = true 428 | 429 | ctx.saveSession = async function saveSession() { 430 | // make sure that the session has been loaded 431 | await ctx.session 432 | 433 | const result = await getSession(ctx) 434 | if (!result) { 435 | return next() 436 | } 437 | return refreshSession(ctx, ctx.session, result.originalHash, result.isNew) 438 | } 439 | 440 | ctx.regenerateSession = async function regenerateSession() { 441 | debug('regenerating session') 442 | // make sure that the session has been loaded 443 | await ctx.session 444 | 445 | if (!isNew) { 446 | // destroy the old session 447 | debug('destroying previous session') 448 | await store.destroy(ctx.sessionId) 449 | } 450 | 451 | ctx._session = generateSession() 452 | ctx.sessionId = await genSid.call(ctx, 24) 453 | sessionIdStore.reset.call(ctx) 454 | debug('created new session: %s', ctx.sessionId) 455 | isNew = true 456 | return ctx._session 457 | } 458 | 459 | await next() 460 | 461 | if (touchSession) { 462 | // if only this.session=, need try to decode and get the sessionID 463 | if (!getter) { 464 | ctx.sessionId = sessionIdStore.get.call(ctx) 465 | } 466 | 467 | await refreshSession(ctx, ctx._session, originalHash, isNew) 468 | } 469 | } 470 | } 471 | 472 | /** 473 | * get the hash of a session include cookie options. 474 | */ 475 | function hash(sess) { 476 | return crc32.signed(JSON.stringify(sess)) 477 | } 478 | 479 | /** 480 | * cookie use maxAge, hack to compat connect type `maxage` 481 | */ 482 | function compatMaxage(opts) { 483 | if (opts) { 484 | opts.maxAge = opts.maxage ? opts.maxage : opts.maxAge 485 | delete opts.maxage 486 | } 487 | } 488 | 489 | module.exports.MemoryStore = MemoryStore 490 | 491 | function defaultErrorHandler (err, type) { 492 | try { 493 | err.name = 'koa-generic-session ' + type + ' error' 494 | } catch (_err) { 495 | // in case there is no `name` setter available (may only have a getter) 496 | } 497 | throw err 498 | } 499 | 500 | function noop () { 501 | return true 502 | } 503 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - lib/store.js 3 | * Copyright(c) 2014 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 'use strict' 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | 16 | const EventEmitter = require('events').EventEmitter 17 | const debug = require('debug')('koa-generic-session:store') 18 | const copy = require('copy-to') 19 | 20 | const defaultOptions = { 21 | prefix: 'koa:sess:' 22 | } 23 | 24 | class Store extends EventEmitter { 25 | constructor(client, options) { 26 | super() 27 | this.client = client 28 | this.options = options 29 | copy(options).and(defaultOptions).to(this.options) 30 | 31 | // delegate client connect / disconnect event 32 | if (typeof client.on === 'function') { 33 | client.on('disconnect', this.emit.bind(this, 'disconnect')) 34 | client.on('connect', this.emit.bind(this, 'connect')) 35 | } 36 | } 37 | 38 | async get(sid) { 39 | sid = this.options.prefix + sid 40 | debug('GET %s', sid) 41 | const data = await this.client.get(sid) 42 | if (!data) { 43 | debug('GET empty') 44 | return null 45 | } 46 | if (data && data.cookie && typeof data.cookie.expires === 'string') { 47 | // make sure data.cookie.expires is a Date 48 | data.cookie.expires = new Date(data.cookie.expires) 49 | } 50 | debug('GOT %j', data) 51 | return data 52 | } 53 | 54 | async set(sid, sess) { 55 | let ttl = typeof this.options.ttl === 'function' ? this.options.ttl(sess) : this.options.ttl; 56 | if (!ttl) { 57 | const maxAge = sess.cookie && sess.cookie.maxAge 58 | if (typeof maxAge === 'number') { 59 | ttl = maxAge 60 | } 61 | // if has cookie.expires, ignore cookie.maxAge 62 | if (sess.cookie && sess.cookie.expires) { 63 | ttl = Math.ceil(sess.cookie.expires.getTime() - Date.now()) 64 | } 65 | } 66 | 67 | sid = this.options.prefix + sid 68 | debug('SET key: %s, value: %s, ttl: %d', sid, sess, ttl) 69 | await this.client.set(sid, sess, ttl) 70 | debug('SET complete') 71 | } 72 | 73 | async destroy(sid) { 74 | sid = this.options.prefix + sid 75 | debug('DEL %s', sid) 76 | await this.client.destroy(sid) 77 | debug('DEL %s complete', sid) 78 | } 79 | } 80 | 81 | module.exports = Store 82 | -------------------------------------------------------------------------------- /test/defer.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/defer.test.js 3 | * Copyright(c) 2013 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | const app = require('./support/defer') 15 | const request = require('supertest') 16 | 17 | describe('test/defer.test.js', () => { 18 | 19 | describe('use', () => { 20 | let cookie 21 | const mockCookie = 'koa.sid=s:dsfdss.PjOnUyhFG5bkeHsZ1UbEY7bDerxBINnZsD5MUguEph8; path=/; httponly' 22 | 23 | it('should GET /session/get ok', async () => { 24 | 25 | const res = await request(app) 26 | .get('/session/get') 27 | .expect(/1/) 28 | 29 | cookie = res.headers['set-cookie'].join(';') 30 | }) 31 | 32 | it('should GET /session/get second ok', () => { 33 | return request(app) 34 | .get('/session/get') 35 | .set('cookie', cookie) 36 | .expect(/2/) 37 | }) 38 | 39 | it('should GET /session/httponly ok', async () => { 40 | const res = await request(app) 41 | .get('/session/httponly') 42 | .set('cookie', cookie) 43 | .expect(/httpOnly: false/) 44 | 45 | cookie = res.headers['set-cookie'].join(';') 46 | cookie.indexOf('httponly').should.equal(-1) 47 | cookie.indexOf('expires=').should.above(0) 48 | 49 | await request(app) 50 | .get('/session/get') 51 | .set('cookie', cookie) 52 | .expect(/3/) 53 | }) 54 | 55 | it('should GET /session/httponly twice ok', async () => { 56 | 57 | const res = await request(app) 58 | .get('/session/httponly') 59 | .set('cookie', cookie) 60 | .expect(/httpOnly: true/) 61 | 62 | cookie = res.headers['set-cookie'].join(';') 63 | cookie.indexOf('httponly').should.above(0) 64 | cookie.indexOf('expires=').should.above(0) 65 | }) 66 | 67 | 68 | it('should another user GET /session/get ok', () => { 69 | return request(app) 70 | .get('/session/get') 71 | .expect(/1/) 72 | }) 73 | 74 | it('should GET /session/nothing ok', () => { 75 | return request(app) 76 | .get('/session/nothing') 77 | .set('cookie', cookie) 78 | .expect(/3/) 79 | }) 80 | 81 | it('should GET /session/notuse response no session', () => { 82 | return request(app) 83 | .get('/session/notuse') 84 | .set('cookie', cookie) 85 | .expect(/no session/) 86 | }) 87 | 88 | it('should GET /wrongpath response no session', () => { 89 | return request(app) 90 | .get('/wrongpath') 91 | .set('cookie', cookie) 92 | .expect(/no session/) 93 | }) 94 | 95 | it('should wrong cookie GET /session/get ok', () => { 96 | return request(app) 97 | .get('/session/get') 98 | .set('cookie', mockCookie) 99 | .expect(/1/) 100 | }) 101 | 102 | it('should wrong cookie GET /session/get twice ok', () => { 103 | return request(app) 104 | .get('/session/get') 105 | .set('cookie', mockCookie) 106 | .expect(/1/) 107 | }) 108 | 109 | it('should GET /session/remove ok', async () => { 110 | await request(app) 111 | .get('/session/remove') 112 | .set('cookie', cookie) 113 | .expect(/0/) 114 | 115 | await request(app) 116 | .get('/session/get') 117 | .set('cookie', cookie) 118 | .expect(/1/) 119 | }) 120 | 121 | it('should GET / error by session ok', () => { 122 | return request(app) 123 | .get('/') 124 | .expect(/no session/) 125 | }) 126 | 127 | it('should GET /session ok', () => { 128 | return request(app) 129 | .get('/session') 130 | .expect(/has session/) 131 | }) 132 | 133 | it('should GET /session/remove before get ok', () => { 134 | return request(app) 135 | .get('/session/remove') 136 | .expect(/0/) 137 | }) 138 | 139 | it('should rewrite session before get ok', () => { 140 | return request(app) 141 | .get('/session/rewrite') 142 | .expect({ foo: 'bar' }) 143 | }) 144 | 145 | it('should regenerate existing sessions', async () => { 146 | const agent = request.agent(app) 147 | const res1 = await agent 148 | .get('/session/get') 149 | .expect(/.+/) 150 | 151 | const firstId = res1.body 152 | 153 | const res2 = await agent 154 | .get('/session/regenerate') 155 | .expect(/.+/) 156 | 157 | const secondId = res2.body 158 | secondId.should.not.equal(firstId) 159 | }) 160 | 161 | it('should regenerate new sessions', () => { 162 | return request(app) 163 | .get('/session/regenerateWithData') 164 | .expect({ /* foo: undefined, */ hasSession: true }) 165 | }) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /test/override.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/override.test.js 3 | * Copyright(c) 2016 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * Evan King (http://honoredsoft.com) 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | const app = require('./support/override') 15 | const request = require('supertest') 16 | const should = require('should') 17 | 18 | describe('test/override.test.js', () => { 19 | let cookie 20 | 21 | before(async () => { 22 | 23 | const res = await request(app) 24 | .get('/session/update') 25 | .expect(/1/) 26 | 27 | cookie = res.headers['set-cookie'].join(';') 28 | }) 29 | 30 | 31 | async function req(path, expectBody, expectCookie) { 32 | 33 | const res = await request(app) 34 | .get('/session/' + path) 35 | .set('cookie', cookie) 36 | 37 | should(res.text).match(expectBody) 38 | 39 | return expectCookie 40 | ? should.exist(res.headers['set-cookie']) 41 | : should.not.exist(res.headers['set-cookie']) 42 | 43 | } 44 | 45 | it('should save modified session', req.bind(null, 'update', /2, null/, true)) 46 | it('should prevent saving modified session', req.bind(null, 'update/prevent', /3, false/, false)) 47 | it('should force saving unmodified session', req.bind(null, 'read/force', /2, true/, true)) 48 | it('should prevent deleting session', req.bind(null, 'remove/prevent', /0, false/, false)) 49 | it('should not have fresh session', req.bind(null, 'read', /2, null/, false)) 50 | it('should delete session on force-save', req.bind(null, 'remove/force', /0, true/, true)) 51 | it('should have fresh session', req.bind(null, 'read', /0, null/, true)) 52 | 53 | }) 54 | -------------------------------------------------------------------------------- /test/rolling.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/rolling.test.js 3 | * Copyright(c) 2016 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | const app = require('./support/rolling') 15 | const request = require('supertest') 16 | const should = require('should') 17 | 18 | describe('test/rolling.test.js', () => { 19 | let cookie 20 | 21 | beforeEach(async () => { 22 | 23 | const res = await request(app) 24 | .get('/session/get') 25 | .expect(/1/) 26 | 27 | cookie = res.headers['set-cookie'].join(';') 28 | }) 29 | 30 | it('should get session success', () => { 31 | return request(app) 32 | .get('/session/get') 33 | .set('cookie', cookie) 34 | .expect(/2/) 35 | }) 36 | 37 | it('should remove session success', async () => { 38 | 39 | await request(app) 40 | .get('/session/remove') 41 | .set('cookie', cookie) 42 | 43 | await request(app) 44 | .get('/session/get') 45 | .set('cookie', cookie) 46 | .expect(/1/) 47 | }) 48 | 49 | describe('when not modify session', () => { 50 | 51 | it('and session exists get set-cookie', async () => { 52 | 53 | const res = await request(app) 54 | .get('/session/nothing') 55 | .set('cookie', cookie) 56 | 57 | should.exist(res.headers['set-cookie']) 58 | }) 59 | 60 | it('and session not exists don\'t get set-cookie', async () => { 61 | 62 | const res = await request(app) 63 | .get('/session/nothing') 64 | 65 | should.not.exist(res.headers['set-cookie']) 66 | 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/session.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/session.test.js 3 | * Copyright(c) 2013 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | const Session = require('..') 15 | const app = require('./support/server') 16 | const request = require('supertest') 17 | const mm = require('mm') 18 | const should = require('should') 19 | const EventEmitter = require('events').EventEmitter 20 | 21 | describe('test/koa-session.test.js', () => { 22 | describe('init', () => { 23 | afterEach(mm.restore) 24 | 25 | beforeEach(() => { 26 | return request(app) 27 | .get('/session/remive') 28 | .expect(200) 29 | }) 30 | 31 | it('should warn when in production', (done) => { 32 | mm(process.env, 'NODE_ENV', 'production') 33 | mm(console, 'warn', (message) => { 34 | message.should.equal('Warning: koa-generic-session\'s MemoryStore is not\n' + 35 | 'designed for a production environment, as it will leak\n' + 36 | 'memory, and will not scale past a single process.') 37 | done() 38 | }) 39 | 40 | Session({ secret: 'secret' }) 41 | }) 42 | 43 | it('should listen disconnect and connect', () => { 44 | const store = new EventEmitter() 45 | Session({ 46 | secret: 'secret', 47 | store: store 48 | }) 49 | store._events.disconnect.should.be.Function 50 | store._events.connect.should.be.Function 51 | }) 52 | }) 53 | 54 | describe('use', () => { 55 | let cookie 56 | const mockCookie = 'koa.sid=s:dsfdss.PjOnUyhFG5bkeHsZ1UbEY7bDerxBINnZsD5MUguEph8; path=/; httponly' 57 | 58 | it('should GET /session/get ok', async () => { 59 | 60 | const res = await request(app) 61 | .get('/session/get') 62 | .expect(/1/) 63 | cookie = res.headers['set-cookie'].join(';') 64 | 65 | }) 66 | 67 | it('should GET /session/get second ok', () => { 68 | return request(app) 69 | .get('/session/get') 70 | .set('cookie', cookie) 71 | .expect(/2/) 72 | }) 73 | 74 | it('should GET /session/httponly ok', async () => { 75 | 76 | const res = await request(app) 77 | .get('/session/httponly') 78 | .set('cookie', cookie) 79 | .expect(/httpOnly: false/) 80 | 81 | cookie = res.headers['set-cookie'].join(';') 82 | cookie.indexOf('httponly').should.equal(-1) 83 | cookie.indexOf('expires=').should.above(0) 84 | return request(app) 85 | .get('/session/get') 86 | .set('cookie', cookie) 87 | .expect(/3/) 88 | }) 89 | 90 | it('should GET /session/httponly twice ok', async () => { 91 | 92 | const res = await request(app) 93 | .get('/session/httponly') 94 | .set('cookie', cookie) 95 | .expect(/httpOnly: true/) 96 | 97 | cookie = res.headers['set-cookie'].join(';') 98 | cookie.indexOf('httponly').should.above(0) 99 | cookie.indexOf('expires=').should.above(0) 100 | 101 | }) 102 | 103 | it('should another user GET /session/get ok', () => { 104 | return request(app) 105 | .get('/session/get') 106 | .expect(/1/) 107 | }) 108 | 109 | it('should GET /session/nothing ok', () => { 110 | return request(app) 111 | .get('/session/nothing') 112 | .set('cookie', cookie) 113 | .expect(/3/) 114 | }) 115 | 116 | it('should wrong cookie GET /session/get ok', () => { 117 | return request(app) 118 | .get('/session/get') 119 | .set('cookie', mockCookie) 120 | .expect(/1/) 121 | }) 122 | 123 | it('should wrong cookie GET /session/get twice ok', () => { 124 | return request(app) 125 | .get('/session/get') 126 | .set('cookie', mockCookie) 127 | .expect(/1/) 128 | }) 129 | 130 | it('should GET /wrongpath response no session', () => { 131 | return request(app) 132 | .get('/wrongpath') 133 | .set('cookie', cookie) 134 | .expect(/no session/) 135 | }) 136 | 137 | it('should GET /session/remove ok', async () => { 138 | await request(app) 139 | .get('/session/remove') 140 | .set('cookie', cookie) 141 | .expect(/0/) 142 | 143 | await request(app) 144 | .get('/session/get') 145 | .set('cookie', cookie) 146 | .expect(/1/) 147 | }) 148 | 149 | it('should GET / error by session ok', () => { 150 | return request(app) 151 | .get('/') 152 | .expect(/no session/) 153 | }) 154 | 155 | it('should GET /session ok', () => { 156 | return request(app) 157 | .get('/session') 158 | .expect(/has session/) 159 | }) 160 | 161 | it('should rewrite session before get ok', () => { 162 | return request(app) 163 | .get('/session/rewrite') 164 | .expect({ foo: 'bar', path: '/session/rewrite' }) 165 | }) 166 | 167 | it('should regenerate a new session when session invalid', async () => { 168 | 169 | await request(app) 170 | .get('/session/get') 171 | .expect('1') 172 | 173 | await request(app) 174 | .get('/session/nothing?valid=false') 175 | .expect('') 176 | 177 | await request(app) 178 | .get('/session/get') 179 | .expect('1') 180 | 181 | }) 182 | 183 | it('should GET /session ok', () => { 184 | return request(app) 185 | .get('/session/id?test_sid_append=test') 186 | .expect(/test$/) 187 | }) 188 | 189 | it('should force a session id ok', async () => { 190 | const res = await request(app) 191 | .get('/session/get') 192 | .expect(/.*/) 193 | 194 | cookie = res.headers['set-cookie'][0].split(';') 195 | const val = cookie[0].split('=').pop() 196 | 197 | await request(app) 198 | .get('/session/id?force_session_id=' + val) 199 | .expect(new RegExp(val)) 200 | }) 201 | 202 | it('should regenerate existing sessions', async () => { 203 | 204 | const agent = request.agent(app) 205 | const res1 = await agent 206 | .get('/session/get') 207 | .expect(/.+/) 208 | 209 | const firstId = res1.body 210 | const res2 = await agent 211 | .get('/session/regenerate') 212 | .expect(/.+/) 213 | 214 | const secondId = res2.body 215 | secondId.should.not.equal(firstId) 216 | }) 217 | 218 | it('should regenerate a new session', () => { 219 | return request(app) 220 | .get('/session/regenerateWithData') 221 | .expect({ /* foo: undefined, */ hasSession: true }) 222 | }) 223 | 224 | it('should always refreshSession', async () => { 225 | 226 | const res = await request(app) 227 | .get('/session/get_error') 228 | .expect(500) 229 | 230 | const cookie = res.headers['set-cookie'].join(';') 231 | should.exist(cookie) 232 | await request(app) 233 | .get('/session/get') 234 | .set('cookie', cookie) 235 | .expect(/2/) 236 | 237 | }) 238 | 239 | it('should set expires= when destroying a session on all session cookies', async () => { 240 | 241 | const agent = request.agent(app) 242 | 243 | await agent 244 | .get('/session/get') 245 | .expect(/.+/) 246 | 247 | const res = await agent 248 | .get('/session/remove') 249 | .expect(/.+/) 250 | 251 | const cookies = res.headers['set-cookie']; 252 | cookies.length.should.equal(2); 253 | for (const cookie of cookies) { 254 | cookie.should.containEql('expires=Thu, 01 Jan 1970 00:00:00 GMT;') 255 | } 256 | }) 257 | }) 258 | }) 259 | -------------------------------------------------------------------------------- /test/store.test.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/store.test.js 3 | * Copyright(c) 2013 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | const deferApp = require('./support/defer') 15 | const commonApp = require('./support/server') 16 | const request = require('supertest') 17 | const mm = require('mm') 18 | 19 | describe('test/store.test.js', () => { 20 | afterEach(mm.restore) 21 | describe('common session middleware', () => { 22 | afterEach(() => { 23 | commonApp.store.emit('connect') 24 | mm.restore() 25 | }) 26 | let cookie 27 | 28 | it('should get session error when disconnect', () => { 29 | commonApp.store.emit('disconnect') 30 | return request(commonApp) 31 | .get('/session/get') 32 | .expect(500) 33 | .expect('session store is unavailable') 34 | }) 35 | 36 | it('should get session ok when reconnect', () => { 37 | commonApp.store.emit('disconnect') 38 | setTimeout(() => { 39 | commonApp.store.emit('connect') 40 | }, 10) 41 | return request(commonApp) 42 | .get('/session/get') 43 | .expect(200) 44 | .expect('1') 45 | }) 46 | 47 | it('should ignore disconnect event', () => { 48 | commonApp.store.emit('disconnect') 49 | commonApp.store.emit('disconnect') 50 | return request(commonApp) 51 | .get('/session/get') 52 | .expect(500) 53 | .expect('session store is unavailable') 54 | }) 55 | 56 | it('should error when status is unavailable', (done) => { 57 | commonApp.store.emit('disconnect') 58 | setTimeout(() => { 59 | request(commonApp) 60 | .get('/session/get') 61 | .expect(500) 62 | .expect('session store is unavailable', done) 63 | }, 200) 64 | }) 65 | 66 | it('should get session ok when store.get error but session not exist', async () => { 67 | mm.error(commonApp.store, 'get', 'mock error') 68 | const res = await request(commonApp) 69 | .get('/session/get') 70 | .expect(/1/) 71 | .expect(200) 72 | cookie = res.headers['set-cookie'].join(';') 73 | cookie.indexOf('httponly').should.above(0) 74 | cookie.indexOf('expires=').should.above(0) 75 | }) 76 | 77 | it('should get session error when store.get error', () => { 78 | mm(commonApp.store, 'get', () => { 79 | throw new Error('mock get error') 80 | }) 81 | return request(commonApp) 82 | .get('/session/get') 83 | .set('cookie', cookie) 84 | .expect(500) 85 | }) 86 | 87 | it('should get /session/notuse error when store.get error', () => { 88 | mm(commonApp.store, 'get', () => { 89 | throw new Error('mock get error') 90 | }) 91 | return request(commonApp) 92 | .get('/session/notuse') 93 | .set('cookie', cookie) 94 | .expect(500) 95 | }) 96 | 97 | it('should handler session error when store.set error', async () => { 98 | await request(commonApp) 99 | .get('/session/get') 100 | .set('cookie', cookie) 101 | .expect(200) 102 | .expect(/2/) 103 | mm(commonApp.store, 'set', () => { 104 | throw new Error('mock set error') 105 | }) 106 | await request(commonApp) 107 | .get('/session/get') 108 | .set('cookie', cookie) 109 | .expect(500) 110 | .expect('mock set error') 111 | 112 | }) 113 | 114 | it('should handler session error when store.set error and logic error', () => { 115 | mm(commonApp.store, 'set', () => { 116 | throw new Error('mock set error') 117 | }) 118 | return request(commonApp) 119 | .get('/session/get_error') 120 | .expect(500) 121 | .expect('oops') 122 | }) 123 | }) 124 | 125 | describe('defer session middleware', () => { 126 | afterEach(() => { 127 | deferApp.store.emit('connect') 128 | mm.restore() 129 | }) 130 | let cookie 131 | const mockCookie = 'koa.sid=s:dsfdss.PjOnUyhFG5bkeHsZ1UbEY7bDerxBINnZsD5MUguEph8; path=/; httponly' 132 | 133 | it('should get session error when disconnect', () => { 134 | deferApp.store.emit('disconnect') 135 | return request(deferApp) 136 | .get('/session/get') 137 | .expect(500) 138 | .expect('session store is unavailable') 139 | }) 140 | 141 | it('should get session ok when store.get error but session not exist', async () => { 142 | mm.error(deferApp.store, 'get', 'mock error') 143 | const res = await request(deferApp) 144 | .get('/session/get') 145 | .expect(/1/) 146 | .expect(200) 147 | 148 | cookie = res.headers['set-cookie'].join(';') 149 | cookie.indexOf('httponly').should.above(0) 150 | cookie.indexOf('expires=').should.above(0) 151 | 152 | }) 153 | 154 | it('should get session error when store.get error', () => { 155 | mm(deferApp.store, 'get', () => { 156 | throw new Error('mock get error') 157 | }) 158 | return request(deferApp) 159 | .get('/session/get') 160 | .set('cookie', cookie) 161 | .expect(500) 162 | }) 163 | 164 | it('should get /session/notuse ok when store.get error', () => { 165 | mm(deferApp.store, 'get', () => { 166 | throw new Error('mock get error') 167 | }) 168 | return request(deferApp) 169 | .get('/session/notuse') 170 | .set('cookie', cookie) 171 | .expect(200) 172 | }) 173 | 174 | it('should handler session error when store.set error', (done) => { 175 | cookie = 'koss:test_sid=bGX1r5jcQHX4CqO1Heiy_DLvkTpQvx3M; path=/session; expires=Thu, 13 Apr 2017 17:19:04 GMT; httponly;koss:test_sid.sig=ZvZ8W0x9akbySx-9kEUkVPqAd2g; path=/session; expires=Thu, 13 Apr 2017 17:19:04 GMT; httponly' 176 | request(commonApp) 177 | .get('/session/get') 178 | .set('cookie', cookie) 179 | .expect(200) 180 | .expect(/2/, () => { 181 | mm(commonApp.store, 'set', () => { 182 | throw new Error('mock set error') 183 | }) 184 | request(commonApp) 185 | .get('/session/get') 186 | .set('cookie', cookie) 187 | .expect(500) 188 | .expect('mock set error', done) 189 | }) 190 | }) 191 | 192 | }) 193 | 194 | }) 195 | -------------------------------------------------------------------------------- /test/support/defer.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/support/defer.js 3 | * Copyright(c) 2013 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | var koa = require('koa'); 16 | var http = require('http'); 17 | var session = require('../../lib/session'); 18 | var Store = require('./store'); 19 | 20 | var app = new koa(); 21 | 22 | app.name = 'koa-session-test'; 23 | app.outputErrors = true; 24 | app.keys = ['keys', 'keykeys']; 25 | app.proxy = true; // to support `X-Forwarded-*` header 26 | 27 | app.use(async (ctx, next) => { 28 | try { 29 | await next(); 30 | } catch (err) { 31 | ctx.status = err.status || 500; 32 | ctx.body = err.message; 33 | } 34 | }); 35 | 36 | var store = new Store(); 37 | app.use(session({ 38 | key: 'koss:test_sid', 39 | cookie: { 40 | maxAge: 86400, 41 | path: '/session', 42 | }, 43 | defer: true, 44 | store: store, 45 | reconnectTimeout: 100 46 | })); 47 | 48 | // will ignore repeat session 49 | app.use(session({ 50 | key: 'koss:test_sid', 51 | cookie: { 52 | maxAge: 86400, 53 | path: '/session' 54 | }, 55 | defer: true 56 | })); 57 | 58 | app.use(async function controllers(ctx) { 59 | switch (ctx.request.url) { 60 | case '/favicon.ico': 61 | ctx.status = 404; 62 | break; 63 | case '/wrongpath': 64 | ctx.body = ctx.session ? 'has session' : 'no session'; 65 | break; 66 | case '/session/rewrite': 67 | ctx.session = { foo: 'bar' }; 68 | ctx.body = await ctx.session; 69 | break; 70 | case '/session/notuse': 71 | nosession(ctx); 72 | break; 73 | case '/session/get': 74 | await get(ctx); 75 | break; 76 | case '/session/nothing': 77 | await nothing(ctx); 78 | break; 79 | case '/session/remove': 80 | await remove(ctx); 81 | break; 82 | case '/session/httponly': 83 | await switchHttpOnly(ctx); 84 | break; 85 | case '/session/regenerate': 86 | await regenerate(ctx); 87 | break; 88 | case '/session/regenerateWithData': 89 | let session = await ctx.session; 90 | session.foo = 'bar'; 91 | session = await regenerate(ctx); 92 | ctx.body = { foo : session.foo, hasSession: session !== undefined }; 93 | break; 94 | default: 95 | await other(ctx); 96 | } 97 | }); 98 | 99 | function nosession(ctx) { 100 | ctx.body = ctx._session !== undefined ? 'has session' : 'no session'; 101 | } 102 | 103 | async function nothing(ctx) { 104 | ctx.body = String((await ctx.session).count); 105 | } 106 | 107 | async function get(ctx) { 108 | let session = await ctx.session; 109 | session = await ctx.session; 110 | session.count = session.count || 0; 111 | session.count++; 112 | ctx.body = String(session.count); 113 | } 114 | 115 | function remove(ctx) { 116 | ctx.session = null; 117 | ctx.body = 0; 118 | } 119 | 120 | async function switchHttpOnly(ctx) { 121 | const session = await ctx.session; 122 | const httpOnly = session.cookie.httpOnly; 123 | session.cookie.httpOnly = !httpOnly; 124 | ctx.body = 'httpOnly: ' + !httpOnly; 125 | } 126 | 127 | function other(ctx) { 128 | ctx.body = ctx.session ? 'has session' : 'no session'; 129 | } 130 | 131 | async function regenerate(ctx) { 132 | const session = await ctx.regenerateSession(); 133 | session.data = 'foo'; 134 | ctx.body = ctx.sessionId; 135 | return session; 136 | } 137 | 138 | // app.listen(7001) 139 | var app = module.exports = http.createServer(app.callback()); 140 | app.store = store; 141 | -------------------------------------------------------------------------------- /test/support/override.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/support/override.js 3 | * Copyright(c) 2016 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * Evan King (http://honoredsoft.com) 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | var koa = require('koa'); 16 | var http = require('http'); 17 | var session = require('../../lib/session'); 18 | var Store = require('./store'); 19 | 20 | var app = new koa(); 21 | 22 | app.name = 'koa-session-test'; 23 | app.outputErrors = true; 24 | app.keys = ['keys', 'keykeys']; 25 | app.proxy = true; // to support `X-Forwarded-*` header 26 | 27 | var store = new Store(); 28 | 29 | app.use(session({ 30 | key: 'koss:test_sid', 31 | prefix: 'koss:test', 32 | ttl: 1000, 33 | cookie: { 34 | maxAge: 86400, 35 | path: '/session' 36 | }, 37 | store: store, 38 | rolling: false, 39 | })); 40 | 41 | app.use(function controllers(ctx) { 42 | switch (ctx.request.path) { 43 | case '/session/read/force': 44 | ctx.sessionSave = true; 45 | case '/session/read': 46 | read(ctx); 47 | break; 48 | 49 | case '/session/update/prevent': 50 | ctx.sessionSave = false; 51 | case '/session/update': 52 | update(ctx); 53 | break; 54 | 55 | case '/session/remove/prevent': 56 | ctx.sessionSave = false; 57 | remove(ctx); 58 | break; 59 | 60 | case '/session/remove/force': 61 | ctx.sessionSave = true; 62 | remove(ctx); 63 | break; 64 | } 65 | 66 | ctx.body = ctx.body + ', ' + ctx.sessionSave; 67 | }); 68 | 69 | function read(ctx) { 70 | ctx.session.count = ctx.session.count || 0; 71 | ctx.body = String(ctx.session.count); 72 | } 73 | 74 | function update(ctx) { 75 | ctx.session.count = ctx.session.count || 0; 76 | ctx.session.count++; 77 | ctx.body = String(ctx.session.count); 78 | } 79 | 80 | function remove(ctx) { 81 | ctx.session = null; 82 | ctx.body = '0'; 83 | } 84 | 85 | var app = module.exports = http.createServer(app.callback()); 86 | -------------------------------------------------------------------------------- /test/support/rolling.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/support/rolling.js 3 | * Copyright(c) 2016 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | var koa = require('koa'); 16 | var http = require('http'); 17 | var session = require('../../lib/session'); 18 | var Store = require('./store'); 19 | 20 | var app = new koa(); 21 | 22 | app.name = 'koa-session-test'; 23 | app.outputErrors = true; 24 | app.keys = ['keys', 'keykeys']; 25 | app.proxy = true; // to support `X-Forwarded-*` header 26 | 27 | var store = new Store(); 28 | 29 | app.use(session({ 30 | key: 'koss:test_sid', 31 | prefix: 'koss:test', 32 | ttl: 1000, 33 | cookie: { 34 | maxAge: 86400, 35 | path: '/session' 36 | }, 37 | store: store, 38 | rolling: true, 39 | })); 40 | 41 | app.use(function controllers(ctx) { 42 | switch (ctx.request.path) { 43 | case '/session/get': 44 | get(ctx); 45 | break; 46 | case '/session/remove': 47 | remove(ctx); 48 | break; 49 | case '/session/nothing': 50 | nothing(ctx); 51 | } 52 | }); 53 | 54 | function get(ctx) { 55 | ctx.session.count = ctx.session.count || 0; 56 | ctx.session.count++; 57 | ctx.body = ctx.session.count; 58 | } 59 | 60 | function remove(ctx) { 61 | ctx.session = null; 62 | ctx.body = 0; 63 | } 64 | 65 | function nothing(ctx) { 66 | ctx.body = 'do not touch session'; 67 | } 68 | 69 | var app = module.exports = http.createServer(app.callback()); 70 | -------------------------------------------------------------------------------- /test/support/server.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * koa-generic-session - test/support/server.js 3 | * Copyright(c) 2013 4 | * MIT Licensed 5 | * 6 | * Authors: 7 | * dead_horse (http://deadhorse.me) 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /** 13 | * Module dependencies. 14 | */ 15 | var koa = require('koa'); 16 | var http = require('http'); 17 | var uid = require('uid-safe'); 18 | var session = require('../../lib/session'); 19 | var Store = require('./store'); 20 | 21 | var app = new koa(); 22 | 23 | app.name = 'koa-session-test'; 24 | app.outputErrors = true; 25 | app.keys = ['keys', 'keykeys']; 26 | app.proxy = true; // to support `X-Forwarded-*` header 27 | 28 | var store = new Store(); 29 | 30 | app.use(async (ctx, next) => { 31 | try { 32 | await next(); 33 | } catch (err) { 34 | ctx.status = err.status || 500; 35 | ctx.body = err.message; 36 | } 37 | }) 38 | 39 | app.use((ctx, next) => { 40 | if (ctx.request.query.force_session_id) { 41 | ctx.sessionId = ctx.request.query.force_session_id; 42 | } 43 | return next(); 44 | }) 45 | 46 | app.use(session({ 47 | key: 'koss:test_sid', 48 | prefix: 'koss:test', 49 | ttl: 1000, 50 | cookie: { 51 | maxAge: 86400, 52 | path: '/session' 53 | }, 54 | store: store, 55 | genSid: async function(len) { 56 | const str = await uid(len); 57 | return str + getSidAppend(this.request.query.test_sid_append); 58 | }, 59 | beforeSave: function (ctx, session) { 60 | session.path = ctx.path; 61 | }, 62 | valid: function (ctx, session) { 63 | return ctx.query.valid !== 'false'; 64 | }, 65 | reconnectTimeout: 100 66 | })); 67 | 68 | // will ignore repeat session 69 | app.use(session({ 70 | key: 'koss:test_sid', 71 | cookie: { 72 | maxAge: 86400, 73 | path: '/session' 74 | }, 75 | genSid: async function(len) { 76 | const str = await uid(len); 77 | return str + getSidAppend(this.request.query.test_sid_append); 78 | } 79 | })); 80 | 81 | app.use(async function controllers(ctx) { 82 | switch (ctx.request.path) { 83 | case '/favicon.ico': 84 | ctx.status = 404; 85 | break; 86 | case '/wrongpath': 87 | ctx.body = !ctx.session ? 'no session' : 'has session'; 88 | break; 89 | case '/session/rewrite': 90 | ctx.session = { foo: 'bar' }; 91 | ctx.body = ctx.session; 92 | break; 93 | case '/session/notuse': 94 | ctx.body = 'not touch session'; 95 | break; 96 | case '/session/get': 97 | get(ctx); 98 | break; 99 | case '/session/get_error': 100 | getError(ctx); 101 | break; 102 | case '/session/nothing': 103 | nothing(ctx); 104 | break; 105 | case '/session/remove': 106 | remove(ctx); 107 | break; 108 | case '/session/httponly': 109 | switchHttpOnly(ctx); 110 | break; 111 | case '/session/id': 112 | getId(ctx); 113 | break; 114 | case '/session/regenerate': 115 | await regenerate(ctx); 116 | break; 117 | case '/session/regenerateWithData': 118 | ctx.session.foo = 'bar'; 119 | await regenerate(ctx); 120 | ctx.body = { foo: ctx.session.foo, hasSession: ctx.session !== undefined }; 121 | break; 122 | default: 123 | other(ctx); 124 | } 125 | }); 126 | 127 | function nothing(ctx) { 128 | ctx.body = ctx.session.count; 129 | } 130 | 131 | function get(ctx) { 132 | ctx.session.count = ctx.session.count || 0; 133 | ctx.session.count++; 134 | ctx.body = String(ctx.session.count); 135 | } 136 | 137 | function getError(ctx) { 138 | ctx.session.count = ctx.session.count || 0; 139 | ctx.session.count++; 140 | throw new Error('oops'); 141 | } 142 | 143 | function remove(ctx) { 144 | ctx.session = null; 145 | ctx.body = '0'; 146 | } 147 | 148 | function switchHttpOnly(ctx) { 149 | var httpOnly = ctx.session.cookie.httpOnly; 150 | ctx.session.cookie.httpOnly = !httpOnly; 151 | ctx.body = 'httpOnly: ' + !httpOnly; 152 | } 153 | 154 | function other(ctx) { 155 | ctx.body = ctx.session !== undefined ? 'has session' : 'no session'; 156 | } 157 | 158 | function getId(ctx) { 159 | ctx.body = ctx.sessionId; 160 | } 161 | 162 | async function regenerate(ctx) { 163 | await ctx.regenerateSession(); 164 | ctx.session.data = 'foo'; 165 | getId(ctx); 166 | } 167 | 168 | function getSidAppend(append) { 169 | return append === undefined ? '' : append; 170 | } 171 | 172 | var app = module.exports = http.createServer(app.callback()); 173 | app.store = store; 174 | -------------------------------------------------------------------------------- /test/support/store.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * koa-generic-session - test/support/store.js 3 | * Copyright(c) 2014 dead_horse 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | const EventEmitter = require('events').EventEmitter; 14 | 15 | class Store extends EventEmitter { 16 | constructor(...args) { 17 | super(...args); 18 | this.sessions = {}; 19 | } 20 | 21 | get(sid) { 22 | const session = this.sessions[sid]; 23 | if (!session) { 24 | return null; 25 | } 26 | const r = {}; 27 | for (const key in session) { 28 | r[key] = session[key]; 29 | } 30 | return r; 31 | } 32 | 33 | set(sid, val) { 34 | this.sessions[sid] = val; 35 | } 36 | 37 | destroy(sid) { 38 | delete this.sessions[sid]; 39 | } 40 | } 41 | 42 | module.exports = Store; 43 | --------------------------------------------------------------------------------