diff --git a/README.md b/README.md index 73a414a..67514c2 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![yar Logo](https://raw.github.com/spumko/yar/master/images/yar.png) -A [**hapi**](https://github.com/spumko/hapi) cookie jar +A [**hapi**](https://github.com/spumko/hapi) session plugin and cookie jar [![Build Status](https://secure.travis-ci.org/spumko/yar.png)](http://travis-ci.org/spumko/yar) @@ -14,45 +14,34 @@ A [**hapi**](https://github.com/spumko/hapi) cookie jar ## Usage -The ***yar*** plugin adds a simple way to set a persistant state (using an [Iron](https://github.com/hueniverse/iron) encrypted cookie) across requests. -It has support for session management - either stored on the client via cookie, in server memory, or using an external database (via custom storage code). +The ***yar*** [hapi](https://github.com/spumko/hapi) plugin adds session support - a persistant state across multiple browser requests using an [iron](https://github.com/hueniverse/iron) encrypted cookie and server-side storage. **yar** tries to fit session data into a session cookie based on a configured maximum size. If the content is too big to fit, it uses local storage via the hapi cache interface (or a [catbox](https://github.com/spumko/catbox) compatible store). -For example, the first handler sets the jar content and the second utilizes it: +For example, the first handler sets a session key and the second gets it: ```javascript var handler1 = function () { - this.plugins.yar = { - key: 'value' - }; - + this.session.set('example', { key: 'value' }; return this.reply(); }; var handler2 = function () { - this.reply(this.state.yar.key); // Will send back 'value' + var example = this.session.get('example'); + this.reply(example.key); // Will send back 'value' }; ``` The plugin requires a password for encryption, and the `ext` permission: ```javascript var options = { - permissions: { - ext: true // Required - }, - plugin: { - name: 'yar' , // Optional, overrides cookie name. Defaults to 'yar'. Doesn't affect 'plugins.yar'. - isSingleUse: false, // Optional, clears jar after one request. Defaults to false. - cookieOptions: { - password: 'password', // Required - isSecure: true // Optional, any supported cookie options except `encoding` - } + cookieOptions: { + password: 'password' } }; var server = new Hapi.Server(); -server.plugin().require('yar', options, function (err) { }); +server.plugin.allow({ ext: true }).require('yar', options, function (err) { }); ``` @@ -60,72 +49,25 @@ server.plugin().require('yar', options, function (err) { }); ### Options -- `name` - determines what name to use for the cookie and module references. Defaults to _yar_. Should not have to modify this unless it conflicts with another plugin named yar. -- `isSingleUse` - determines whether the cookie should be deleted on next request. Defaults to _false_. -- `cookieOptions` - the configuration for cookie-specific features - - `password` - (Required) used to hash and secure the cookie data +- `name` - determines the name of the cookie used to store session information. Defaults to _session_. +- `ttl` - server-side storage expiration (defaults to 1 day). Not used with custom storage. +- `maxCookieSize` - maximum cookie size before using server-side storage. Defaults to 1K. Set to zero to always use server-side storage. +- `storage` - Catbox-compatible storage to be used instead of the hapi internal cache. +- `cookieOptions` - the configuration for cookie-specific features: + - `password` - (Required) used to encrypt and sign the cookie data. - `path` - determines the cookie path. Defaults to _'/'_. - - `isSecure` - determines whether or not to transfer using TLS/SSL. Defaults to _false_. -- `session` - determines whether to enable the more robust session features (any non false-y values will enable it). Defaults to _false_. - - `key` - determines how to access the `request.session` object. Defaults to _'session'_. - - `sidKey` - determines what key to use for storing session id in session object. Defaults to _'sid'_. - - `startKey` - determines what key to use for storing server start time in session object (used for identifying stale cookies). Defaults to _'sst'_. - - `maxLen` - determines the maximum string length allowed in a cookie before falling back to MemoryStore - - `store` - setting this to an MemoryStore compatible interface will allow session data to be stored externally. Defaults to _null_. - - -### Sessions - -More robust session support is included in yar but it is not enabled by default. To enable, simple set the plugin option session to true: - -```javascript -var options = { - "cookieOptions": { - "password": "worldofwalmart" - }, - "session": true -}; -``` + - `isSecure` - determines whether or not to transfer using TLS/SSL. Defaults to _true_. #### Methods -This will enable several request-level methods and parameters: - -* request.session -* request.flash - -##### request.session - -Session support will enable the `request.session` object. Modifications to this object will persist between requests for a given user. The objects are not shared between users. The objects are stored entirely within the user cookie **UNLESS** the size exceeds `session.maxLen` - at which point, they will be stored on the server in RAM. - -**Basic example** - -```javascript -server.addRoute({ - method: 'GET', - path: '/', - config: { - handler: function (request) { - - if (!request.session.loggedIn) { - request.session.loggedIn = true; // logging you in - request.reply.redirect('/').send(); - } - else { - request.reply("You are logged in"); - } - } - } -}); -``` - -##### request.flash(type, message) - -Session support will also enable the `flash` function. The flash function is used to store volatile data - data that should be deleted once read. - -When given no arguments, it will return all of the flash messages and delete the originals. - -When given only a type, it will return all of the flash messages of that type and delete the originals. +**yar** adds the `session` property to every request object and initializes the `session.id` on the first request from each browser. The `request.session` interface provides the following methods: -When given a type and a message, it will set or append that message to the given type. \ No newline at end of file +- `reset()` - clears the session and assigns a new session id. +- `set(key, value)` - assigns a value (string, object, etc) to a given key which will persist across requests. +- `set(keysObject)` - assigns values to multiple keys using each 'keysObject' top-level property. +- `get(key, clear)` - retreive value using a key. If 'clear' is 'true', key is cleared on return. +- `clear(key)` - clears key. +- `touch()` - Manually notify the session of changes (when using `get()` and changing the content of the returned reference directly without calling `set()`). +- `flash(type, message, isOverride)` - stores volatile data - data that should be deleted once read. When given no arguments, it will return all of the flash messages and delete the originals. When given only a type, it will return all of the flash messages of that type and delete the originals. When given a type and a message, it will set or append that message to the given type. 'isOverride' used to indicate that the message provided should replace any existing value instead of being appended to it (defaults to false). +- `lazy(enabled)` - if set to 'true', enables lazy mode. In lazy mode, `request.session` can be modified directly (e.g. setting `request.session.myKey` to an object value), and those keys will be stored and loaded back. Lazy mode isn't as fast as the normal get/set because it has to store the session state on every responses regardless of any changes being made. diff --git a/examples/session/index.js b/examples/index.js similarity index 65% rename from examples/session/index.js rename to examples/index.js index 713051b..529b308 100755 --- a/examples/session/index.js +++ b/examples/index.js @@ -3,16 +3,13 @@ var Hapi = require('hapi'); var server = new Hapi.Server(process.env.PORT || 8080); var options = { - // name: 'yar' , // Optional, overrides cookie name. Defaults to 'yar'. Doesn't affect 'plugins.yar'. - // isSingleUse: false, // Optional, clears jar after one request. Defaults to false. cookieOptions: { password: 'password', // Required - // isSecure: true // Optional, any supported cookie options except `encoding` - }, - session: true + isSecure: false // Required if using http + } }; -server.plugin().allow({ ext: true }).require('yar', options, function (err) { +server.plugin.allow({ ext: true }).require('../', options, function (err) { if (err) { console.log(err) @@ -26,7 +23,7 @@ server.route({ config: { handler: function (request) { - request.reply(request.session) + request.reply(request.session._store) } } }); @@ -37,7 +34,7 @@ server.route({ config: { handler: function (request) { - request.session.test = 1; + request.session.set('test', 1); request.reply.redirect('/').send(); } } @@ -49,7 +46,7 @@ server.route({ config: { handler: function (request) { - request.session[request.params.key] = request.params.value; + request.session.set(request.params.key, request.params.value); request.reply.redirect('/').send(); } } @@ -61,7 +58,7 @@ server.route({ config: { handler: function (request) { - request.session = {}; + request.session.reset(); request.reply.redirect('/').send(); } } diff --git a/examples/session/package.json b/examples/package.json similarity index 92% rename from examples/session/package.json rename to examples/package.json index 5a08927..eb00d30 100644 --- a/examples/session/package.json +++ b/examples/package.json @@ -7,6 +7,6 @@ "license": "BSD", "dependencies": { "yar": "0.1.x", - "hapi": "0.14.x" + "hapi": "0.15.x" } } diff --git a/lib/index.js b/lib/index.js index 8e6a1c8..7ec6aa4 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,76 +1,252 @@ // Load modules var Hoek = require('hoek'); -var Session = require('./session'); +var Uuid = require('node-uuid'); // Declare internals var internals = {}; + +// Defaults + internals.defaults = { - name: 'yar', - isSingleUse: false, // Cleared after every request, unless modified. Override via route.config.plugins.yar.retain set to true + name: 'session', // Cookie name + ttl: 24 * 60 * 60 * 1000, // One day session + maxCookieSize: 1024, // Maximum size allowed in a cookie + store: null, // Catbox compatible policy object to be used instead of hapi's cache cookieOptions: { // hapi server.state() options, except 'encoding' which is always 'iron'. 'password' required. - path: '/' - }, - session: false -}; - -internals.sessionDefaults = { - key: 'session', - sidKey: 'sid', - startKey: 'sst', // server start time - maxLen: 2400 // Maximum size allowed in a cookie + path: '/', + isSecure: true + } }; exports.register = function (pack, options, next) { + // Validate options and apply defaults + var settings = Hoek.applyToDefaults(internals.defaults, options); - settings.session = Hoek.applyToDefaults(internals.sessionDefaults, settings.session); + Hoek.assert(!settings.cookieOptions.encoding, 'Cannot override cookie encoding'); + var rawCookieOptions = Hoek.clone(settings.cookieOptions); settings.cookieOptions.encoding = 'iron'; + rawCookieOptions.encoding = 'none'; + // Configure cookie + pack.state(settings.name, settings.cookieOptions); - pack.ext('onPreHandler', function (request, callback) { + // Setup session store + + var startTime = Date.now(); + var cache = (settings.store ? settings.store : pack.cache({ expiresIn: settings.ttl })); - request.state[settings.name] = request.state[settings.name] || {}; - request.plugins.yar = {}; + // Pre auth + + pack.ext('onPreAuth', function (request, callback) { - if (settings.isSingleUse && - !(request.route.plugins.yar && request.route.plugins.yar.retain)) { + // Load session data from cookie - request.clearState(settings.name); - } + var load = function () { - callback(); - }); + request.session = Hoek.clone(request.state[settings.name]); + if (request.session && + request.session.id && + request.session._sst === startTime) { // Check for stale cookie - pack.ext('onPostHandler', function (request, callback) { + request.session._isModified = false; + if (request.session._store) { + return decorate(); + } - if (Object.keys(request.plugins.yar).length) { - request.setState(settings.name, request.plugins.yar); - } + request.session._store = {}; + return cache.get(request.session.id, function (err, cached) { - callback(); - }); + if (err) { + return decorate(err); + } - if (!settings.session) { - return next(); - } + if (cached && cached.item) { + request.session._store = cached.item; + } + + return decorate(); + }); + } + + request.session = { + id: Uuid.v4(), + _sst: startTime, + _store: {}, + _isModified: true + }; + + decorate(); + }; + + var decorate = function (err) { + + if (request.session._store._lazyKeys) { + request.session._isLazy = true; // Default to lazy mode if previously set + request.session._store._lazyKeys.forEach(function (key) { + + request.session[key] = request.session._store[key]; + delete request.session._store[key]; + }); + } + + request.session.reset = function () { + + request.session.id = Uuid.v4(); + request.session._sst = startTime; + request.session._store = {}; + request.session._isModified = true; + cache.drop(request.session.id, function (err) { }); + }; + + request.session.get = function (key, clear) { + + var value = request.session._store[key]; + if (clear) { + request.session.clear(key); + } + + return value; + }; - var session = new Session(settings); - pack.ext('onPreHandler', function () { + request.session.set = function (key, value) { - session.load.apply(session, Array.prototype.slice.call(arguments)); + Hoek.assert(key, 'Missing key'); + Hoek.assert(typeof key === 'string' || (typeof key === 'object' && value === undefined), 'Invalid session.set() arguments'); + + request.session._isModified = true; + + if (typeof key === 'string') { + var holder = {}; + holder[key] = value; + key = holder; + } + + Object.keys(key).forEach(function (name) { + + request.session._store[name] = key[name]; + }); + }; + + request.session.clear = function (key) { + + request.session._isModified = true; + delete request.session._store[key]; + }; + + request.session.touch = function () { + + request.session._isModified = true; + }; + + request.session.flash = function (type, message, isOverride) { + + request.session._isModified = true; + request.session._store._flash = request.session._store._flash || {}; + + if (!type && !message) { + var messages = request.session._store._flash; + request.session._store._flash = {}; + return messages; + } + + if (!message) { + var messages = request.session._store._flash[type]; + delete request.session._store._flash[type]; + return messages || []; + } + + request.session._store._flash[type] = (isOverride ? message : (request.session._store._flash[type] || []).concat(message)); + return request.session._store._flash[type]; + }; + + request.session.lazy = function (enabled) { + + request.session._isLazy = enabled; + }; + + callback(err); + }; + + load(); }); + + // Post handler + + pack.ext('onPostHandler', function (request, callback) { - pack.ext('onPostHandler', function () { + if (!request.session._isModified && + !request.session._isLazy) { - session.save.apply(session, Array.prototype.slice.call(arguments)); + return callback(); + } + + var prepare = function () { + + if (request.session._isLazy) { + var lazyKeys = []; + Object.keys(request.session).forEach(function (key) { + + if (['id', '_sst', '_store', '_isModified', '_isLazy', 'reset', 'get', 'set', 'clear', 'touch', 'flash', 'lazy'].indexOf(key) === -1 && + key[0] !== '_' && + typeof request.session.key !== 'function') { + + lazyKeys.push(key); + request.session._store[key] = request.session[key]; + } + }); + + if (lazyKeys.length) { + request.session._store._lazyKeys = lazyKeys; + } + } + + if (settings.maxCookieSize) { + return cookie(); + } + + return storage(); + }; + + var cookie = function () { + + var content = { + id: request.session.id, + _sst: request.session._sst, + _store: request.session._store + }; + + pack.hapi.state.prepareValue(settings.name, content, settings.cookieOptions, function (err, value) { + + if (err) { + return callback(err); + } + + if (value.length > settings.maxCookieSize) { + return storage(); + } + + request.setState(settings.name, value, rawCookieOptions); + return callback(); + }); + }; + + var storage = function () { + + request.setState(settings.name, { id: request.session.id, _sst: request.session._sst }); + cache.set(request.session.id, request.session._store, 0, callback); + }; + + prepare(); }); next(); }; + diff --git a/lib/request.js b/lib/request.js deleted file mode 100644 index 56ac42e..0000000 --- a/lib/request.js +++ /dev/null @@ -1,28 +0,0 @@ -var RequestMethods = {}; - -RequestMethods.flash = function (request) { - - return function (type, message) { - - request.session.flash = request.session.flash || {}; - - if (!type && !message){ - var messages = request.session.flash || {}; - request.session.flash = {}; - return messages; - } - - if (!message) { - var results = request.session.flash[type] || []; - delete request.session.flash[type]; - return results; - } - - return request.session.flash[type] = (request.session.flash[type] || []).concat(message); - - - }; -}; - - -module.exports = exports = RequestMethods; \ No newline at end of file diff --git a/lib/session.js b/lib/session.js deleted file mode 100755 index 593368c..0000000 --- a/lib/session.js +++ /dev/null @@ -1,158 +0,0 @@ -// Load modules - -var Hoek = require('hoek'); -var Uuid = require('node-uuid'); -var CookieStore = require('./stores/cookie'); -var MemoryStore = require('./stores/memory'); -var RequestMethods = require('./request'); - - -// Declare internals - -var internals = {}; - - -module.exports = internals.Session = function (options) { - - this.settings = options; - - Hoek.assert(this.settings.session && this.settings.session.key, 'No session.key defined (the x in request[x])'); - Hoek.assert(typeof this.settings.session.key === 'string', 'Invalid session.key defined'); - - this.settings.session.startTime = Date.now(); - - if (this.settings.session.store) { - this.extStore = new this.settings.session.store(); - } - - this.cookieStore = new CookieStore(this.settings.session); - this.memoryStore = new MemoryStore(this.settings.session); - - return this; -}; - - -internals.Session.prototype.save = function (request, callback) { - - var self = this; - - var session = request[this.settings.session.key]; - - // Try External Store - - if (this.extStore && - this.extStore.validate(session)) { - - var sid = this.generateSID(session); - session[this.settings.session.sidKey] = sid; - - return this.extStore.get(sid, session, function (err) { - - request.setState(self.settings.name, self.wrap(session)); - process.nextTick(function () { - - callback(err); - }); - }); - } - - // No External Store, try Cookie - - if (this.cookieStore && - this.cookieStore.validate(session)) { - - return this.cookieStore.get(null, session, function (err) { - - request.setState(self.settings.name, self.wrap(session)); - process.nextTick(function () { - - callback(err); - }); - }); - } - - // Fallback to Memory Store - - var sid = this.generateSID(session); - session[this.settings.session.sidKey] = sid; - return this.memoryStore.get(sid, session, function (err) { - - request.setState(self.settings.name, self.wrap(session)); - process.nextTick(function () { - - callback(err); - }); - }); -}; - - -internals.Session.prototype.wrap = function (session) { - - var state = {}; - state[this.settings.session.key] = session; - return state; -}; - - -internals.Session.prototype.attach = function (request, callback) { - - var self = this; - - return function (err, session, req) { - - if (!session) { - var session = self.regenerate(session); - } - request[self.settings.session.key] = session; - callback(); - }; -}; - - -internals.Session.prototype.load = function (request, callback) { - - var self = this; - - // Get initial state from cookie - - var session = request[this.settings.session.key] = request.state[this.settings.name][this.settings.session.key] || {}; - var sid = session[this.settings.session.sidKey]; - session[this.settings.session.startKey] = session[this.settings.session.startKey] || 0; - - // Check for stale cookie (leftover from server restart) - - var fn = 'get'; - if (session[this.settings.session.startKey] != this.settings.session.startTime) { - var fn = 'delete'; - } - - // Augment Request - for (var method in RequestMethods) { - request[method] = RequestMethods[method](request); - } - - // Get Session - - if (this.extStore) { - return this.extStore[fn](sid, this.attach(request, callback)); - } - - return this.memoryStore[fn](sid, this.attach(request, callback)); -}; - - -internals.Session.prototype.generateSID = function (session) { - - session = session || {}; - return session[this.settings.session.sidKey] || Uuid.v4(); -}; - - -internals.Session.prototype.regenerate = function (session) { - - session = session || {}; - session[this.settings.session.sidKey] = this.generateSID(session); - session[this.settings.session.startKey] = this.settings.session.startTime; - return session; -}; - diff --git a/lib/stores/cookie.js b/lib/stores/cookie.js deleted file mode 100755 index 24ac7c1..0000000 --- a/lib/stores/cookie.js +++ /dev/null @@ -1,32 +0,0 @@ -// Load modules - - -// Declare internals - -var internals = {}; - - -module.exports = internals.CookieStore = function (options) { - - this.options = options; - - return this; -}; - - -internals.CookieStore.prototype.validate = function (session) { - - if (!session || (Object.keys(session).length > 0)) { - return false; - } - - var sessionLength = JSON.stringify(session).length; - return (sessionLength < this.options.maxLen); -}; - - -internals.CookieStore.prototype.get = function (key, session, callback) { - - return callback(null, session); -}; - diff --git a/lib/stores/memory.js b/lib/stores/memory.js deleted file mode 100755 index c78e44a..0000000 --- a/lib/stores/memory.js +++ /dev/null @@ -1,44 +0,0 @@ -// Load modules - - -// Declare internals - -var internals = {}; - - -module.exports = internals.MemoryStore = function (options) { - - this.options = options; - this._cache = {}; - - return this; -}; - - -internals.MemoryStore.prototype.get = function (key, session, callback) { - - if (typeof session == 'function') { - callback = session; - session = null; - } - - if (session) { - this._cache[key] = session; - } - - callback(null, this._cache[key]); -}; - - -internals.MemoryStore.prototype.delete = function (key, session, callback) { - - if (typeof session == 'function') { - callback = session; - session = null; - } - - delete this._cache[key]; - - callback(); -}; - diff --git a/package.json b/package.json index f790072..7cfd2f0 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "yar", "description": "Cookie jar plugin", - "version": "0.1.3", + "version": "0.2.0", "author": "Eran Hammer (http://hueniverse.com)", "contributors": [ "Van Nguyen " @@ -23,10 +23,10 @@ "node-uuid": "1.4.x" }, "peerDependencies": { - "hapi": "0.15.x" + "hapi": "0.16.x" }, "devDependencies": { - "hapi": "0.15.x", + "hapi": "0.16.x", "lab": "0.0.x", "complexity-report": "0.x.x" }, diff --git a/test/index.js b/test/index.js index 33edfe9..c75a07a 100755 --- a/test/index.js +++ b/test/index.js @@ -20,35 +20,150 @@ var it = Lab.test; describe('Yar', function () { - it('sets yar then gets it back', function (done) { + it('sets session value then gets it back (store mode)', function (done) { var options = { - name: 'jarx', - isSingleUse: true, + maxCookieSize: 0, cookieOptions: { password: 'password', - isSecure: true + isSecure: false } }; - var server = new Hapi.Server(); + var server = new Hapi.Server(0); server.route([ { method: 'GET', path: '/1', handler: function () { - expect(this.state.jarx).to.deep.equal({}); - expect(this.plugins.yar).to.deep.equal({}); - this.plugins.yar.some = { value: 123 }; + this.session.set('some', { value: '2' }); + this.session.set('one', 'xyz'); + this.session.clear('one'); + return this.reply(Object.keys(this.session._store).length); + } + }, + { + method: 'GET', path: '/2', handler: function () { + + var some = this.session.get('some'); + some.raw = 'access'; + this.session.touch(); + return this.reply(some.value); + } + }, + { + method: 'GET', path: '/3', handler: function () { + + var raw = this.session.get('some').raw; + this.session.reset(); + return this.reply(raw); + } + } + ]); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result).to.equal(1); + var header = res.headers['set-cookie']; + expect(header.length).to.equal(1); + expect(header[0]).to.not.contain('Secure'); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('2'); + var header = res.headers['set-cookie']; + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/3', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('access'); + done(); + }); + }); + }); + }); + }); + }); + + it('sets session value then gets it back (cookie mode)', function (done) { + + var options = { + cookieOptions: { + password: 'password' + } + }; + + var server = new Hapi.Server(0); + + server.route([ + { + method: 'GET', path: '/1', handler: function () { + + this.session.set('some', { value: '2' }); + return this.reply('1'); + } + }, + { + method: 'GET', path: '/2', handler: function () { + + return this.reply(this.session.get('some').value); + } + } + ]); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result).to.equal('1'); + var header = res.headers['set-cookie']; + expect(header.length).to.equal(1); + expect(header[0]).to.contain('Secure'); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('2'); + var header = res.headers['set-cookie']; + done(); + }); + }); + }); + }); + }); + + it('sets session value then gets it back (hybrid mode)', function (done) { + + var options = { + maxCookieSize: 10, + cookieOptions: { + password: 'password' + } + }; + + var server = new Hapi.Server(0); + + server.route([ + { + method: 'GET', path: '/1', handler: function () { + + this.session.set('some', { value: '12345678901234567890' }); return this.reply('1'); } }, { method: 'GET', path: '/2', handler: function () { - expect(this.state.jarx).to.deep.equal({ some: { value: 123 } }); - expect(this.plugins.yar).to.deep.equal({}); - return this.reply('2'); + return this.reply(this.session.get('some').value); } } ]); @@ -56,26 +171,343 @@ describe('Yar', function () { server.plugin.allow({ ext: true }).require('../', options, function (err) { expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/1' }, function (res) { + server.start(function () { - expect(res.result).to.equal('1'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - expect(header[0]).to.contain('Secure'); + server.inject({ method: 'GET', url: '/1' }, function (res) { - var cookie = header[0].match(/(jarx=[^\x00-\x20\"\,\;\\\x7F]*)/); + expect(res.result).to.equal('1'); + var header = res.headers['set-cookie']; + expect(header.length).to.equal(1); + expect(header[0]).to.contain('Secure'); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); - server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { - expect(res.result).to.equal('2'); + expect(res.result).to.equal('12345678901234567890'); + var header = res.headers['set-cookie']; + done(); + }); + }); + }); + }); + }); + + it('sets session value then gets it back (lazy mode)', function (done) { + + var options = { + cookieOptions: { + password: 'password' + } + }; + + var server = new Hapi.Server(0); + + server.route([ + { + method: 'GET', path: '/1', handler: function () { + + this.session.lazy(true); + this.session.some = { value: '2' }; + return this.reply('1'); + } + }, + { + method: 'GET', path: '/2', handler: function () { + + return this.reply(this.session.some.value); + } + } + ]); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result).to.equal('1'); var header = res.headers['set-cookie']; expect(header.length).to.equal(1); - expect(header[0]).to.equal('jarx=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; Path=/'); + expect(header[0]).to.contain('Secure'); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('2'); + var header = res.headers['set-cookie']; + done(); + }); + }); + }); + }); + }); + + it('sets session value then gets it back (clear)', function (done) { + + var options = { + maxCookieSize: 0, + cookieOptions: { + password: 'password', + isSecure: false + } + }; + + var server = new Hapi.Server(0); + + server.route([ + { + method: 'GET', path: '/1', handler: function () { + + this.session.set('some', '2'); + return this.reply('1'); + } + }, + { + method: 'GET', path: '/2', handler: function () { + + var some = this.session.get('some', true); + return this.reply(some); + } + }, + { + method: 'GET', path: '/3', handler: function () { + + var some = this.session.get('some'); + return this.reply(some || '3'); + } + } + ]); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result).to.equal('1'); + var header = res.headers['set-cookie']; + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('2'); + var header = res.headers['set-cookie']; + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/3', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result).to.equal('3'); + done(); + }); + }); + }); + }); + }); + }); + + it('fails to set cookie in invalid cache', function (done) { + + var options = { + maxCookieSize: 0, + cookieOptions: { + password: 'password' + } + }; + + var server = new Hapi.Server(0); + + server.route([ + { + method: 'GET', path: '/1', handler: function () { + + this.session.set('some', { value: '2' }); + return this.reply('1'); + } + }, + { + method: 'GET', path: '/2', handler: function () { + + return this.reply(this.session.get('some').value); + } + } + ]); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + var header = res.headers['set-cookie']; + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.plugin._cache.stop(); + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.statusCode).to.equal(500); + done(); + }); + }); + }); + }); + }); + + it('fails generating session cookie header value (missing password)', function (done) { + + var server = new Hapi.Server(0); + + server.route({ + method: 'GET', path: '/1', handler: function () { + + this.session.set('some', { value: '2' }); + return this.reply('1'); + } + }); + + server.plugin.allow({ ext: true }).require('../', function (err) { + + expect(err).to.not.exist; + server.start(function () { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.statusCode).to.equal(500); done(); }); }); }); }); + + describe("#flash", function () { + + it('should get all flash messages when given no arguments', function (done) { + + var options = { + cookieOptions: { + password: 'password' + } + }; + var server = new Hapi.Server(0); + + server.route({ + method: 'GET', + path: '/1', + config: { + handler: function () { + + this.session.flash('error', 'test error 1'); + this.session.flash('error', 'test error 2'); + this.session.flash('test', 'test 1', true); + this.session.flash('test', 'test 2', true); + this.reply(this.session._store); + } + } + }); + + server.route({ + method: 'GET', + path: '/2', + config: { + handler: function () { + + var flashes = this.session.flash(); + this.reply({ + session: this.session._store, + flashes: flashes + }); + } + } + }); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function (err) { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result._flash.error).to.deep.equal(['test error 1', 'test error 2']); + expect(res.result._flash.test).to.deep.equal('test 2'); + + var header = res.headers['set-cookie']; + expect(header.length).to.equal(1); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result.session._flash.error).to.not.exist; + expect(res.result.flashes).to.exist; + done(); + }); + }); + }); + }); + }); + + it('should delete on read', function (done) { + + var options = { + cookieOptions: { + password: 'password' + } + }; + var server = new Hapi.Server(0); + + server.route({ + method: 'GET', + path: '/1', + config: { + handler: function () { + + this.session.flash('error', 'test error'); + this.reply(this.session._store); + } + } + }); + + server.route({ + method: 'GET', + path: '/2', + config: { + handler: function () { + + var errors = this.session.flash('error'); + this.reply({ + session: this.session._store, + errors: errors + }); + } + } + }); + + server.plugin.allow({ ext: true }).require('../', options, function (err) { + + expect(err).to.not.exist; + server.start(function (err) { + + server.inject({ method: 'GET', url: '/1' }, function (res) { + + expect(res.result._flash.error).to.exist; + expect(res.result._flash.error.length).to.be.above(0); + + var header = res.headers['set-cookie']; + expect(header.length).to.equal(1); + var cookie = header[0].match(/(session=[^\x00-\x20\"\,\;\\\x7F]*)/); + + server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { + + expect(res.result.session._flash.error).to.not.exist; + expect(res.result.errors).to.exist; + done(); + }); + }); + }); + }); + }); + }); }); diff --git a/test/integration/request.js b/test/integration/request.js deleted file mode 100755 index 2256b9f..0000000 --- a/test/integration/request.js +++ /dev/null @@ -1,157 +0,0 @@ -// Load modules - -var Lab = require('lab'); -var Hapi = require('hapi'); - - -// Declare internals - -var internals = {}; - - -// Test shortcuts - -var expect = Lab.expect; -var before = Lab.before; -var after = Lab.after; -var describe = Lab.experiment; -var it = Lab.test; - - -describe('Request', function () { - - describe("#flash", function () { - - it('should get all flash messages when given no arguments', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/1', - config: { - handler: function (request) { - - request.flash('error', 'test error'); - request.reply(JSON.stringify(request.session)); - } - } - }); - - server.route({ - method: 'GET', - path: '/2', - config: { - handler: function (request) { - - var flashes = request.flash(); - request.reply(JSON.stringify({ - session: request.session, - flashes: flashes - })); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - - server.inject({ method: 'GET', url: '/1' }, function (res) { - - var result = JSON.parse(res.result); - expect(result.flash.error).to.exist; - expect(result.flash.error.length).to.be.above(0); - - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { - - var result = JSON.parse(res.result); - expect(result.session.flash.error).to.not.exist; - expect(result.flashes).to.exist; - - done(); - }) - }) - }); - }); - - it('should delete on read', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/1', - config: { - handler: function (request) { - - // request.session.test = 1; - request.flash('error', 'test error'); - // console.log('flashing', request.session) - request.reply(JSON.stringify(request.session)); - } - } - }); - - server.route({ - method: 'GET', - path: '/2', - config: { - handler: function (request) { - - // console.log('about to flash', request.session) - var errors = request.flash('error'); - // console.log('flashed', request.session) - request.reply(JSON.stringify({ - session: request.session, - errors: errors - })); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - - server.inject({ method: 'GET', url: '/1' }, function (res) { - - var result = JSON.parse(res.result); - expect(result.flash.error).to.exist; - expect(result.flash.error.length).to.be.above(0); - - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/2', headers: { cookie: cookie[1] } }, function (res) { - - var result = JSON.parse(res.result); - expect(result.session.flash.error).to.not.exist; - expect(result.errors).to.exist; - - done(); - }) - }) - }); - }); - }); -}); \ No newline at end of file diff --git a/test/integration/session.js b/test/integration/session.js deleted file mode 100755 index 6a82221..0000000 --- a/test/integration/session.js +++ /dev/null @@ -1,376 +0,0 @@ -// Load modules - -var Lab = require('lab'); -var Hapi = require('hapi'); - - -// Declare internals - -var internals = {}; - - -// Test shortcuts - -var expect = Lab.expect; -var before = Lab.before; -var after = Lab.after; -var describe = Lab.experiment; -var it = Lab.test; - - -describe('Session', function () { - - describe("#cookieStore", function () { - - it('should set/get request.session properly using cookie', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/set', - config: { - handler: function (request) { - - request.session.test = 1; - request.reply('ok'); - } - } - }); - - server.route({ - method: 'GET', - path: '/get', - config: { - handler: function (request) { - - request.reply(request.session); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/set' }, function (res) { - - expect(res.result).to.equal('ok'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/get', headers: { cookie: cookie[1] } }, function (res) { - - // expect(res.result).to.contain('test'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - done(); - }); - }); - }); - }); - - it('should not set cookie if given empty session', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/set', - config: { - handler: function (request) { - - request.session = {}; - request.reply('ok'); - } - } - }); - - server.route({ - method: 'GET', - path: '/get', - config: { - handler: function (request) { - - request.reply(request.session); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/set' }, function (res) { - - expect(res.result).to.equal('ok'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/get', headers: { cookie: cookie[1] } }, function (res) { - - // expect(res.result).to.contain('test'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - done(); - }); - }); - }); - }); - }); - - describe("#extStore", function () { - - var extStore = function (options) { - - this.options = options || {}; - this._store = {}; - - return this; - }; - - - extStore.prototype.validate = function (session) { - - if (!session) { - return false; - } - return true; - }; - - - extStore.prototype.get = function (key, session, callback) { - - if (typeof session == 'function') { - callback = session; - session = null; - } - - if (session) { - this._store[key] = session; - } - - return callback(null, this._store[key]); - }; - - - extStore.prototype.delete = function (key, session, callback) { - - if (typeof session == 'function') { - callback = session; - session = null; - } - - delete this._store[key]; - - callback(null, {}); - }; - - - describe('extStore', function () { - - it('should have a validate function', function (done) { - - var sessionstore = new extStore(); - expect(sessionstore.validate).to.exist; - done(); - }); - }); - - - it('should set/get request.session properly using extStore', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: { - store: extStore - } - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/set', - config: { - handler: function (request) { - - request.session.test = 1; - request.reply('ok'); - } - } - }); - - server.route({ - method: 'GET', - path: '/get', - config: { - handler: function (request) { - - request.reply(request.session); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/set' }, function (res) { - - expect(res.result).to.equal('ok'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/get', headers: { cookie: cookie[1] } }, function (res) { - - // expect(res.result).to.contain('test'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - done(); - }); - }); - }); - }); - }); - - describe("#memoryStore", function () { - - var longstr = []; - for(var i = 0; i < 2500; ++i) { - longstr.push('a'); - } - longstr = longstr.join(""); - - it('should set/get request.session properly using memoryStore', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/set', - config: { - handler: function (request) { - - request.session.test = longstr; - request.reply('ok'); - } - } - }); - - server.route({ - method: 'GET', - path: '/get', - config: { - handler: function (request) { - - request.reply(request.session); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/set' }, function (res) { - - expect(res.result).to.equal('ok'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/get', headers: { cookie: cookie[1] } }, function (res) { - - // expect(res.result).to.contain('test'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - done(); - }); - }); - }); - }); - - it('should work even if empty session given', function (done) { - - var options = { - isSingleUse: false, - cookieOptions: { - password: 'password' - }, - session: true - }; - var server = new Hapi.Server(); - - server.route({ - method: 'GET', - path: '/set', - config: { - handler: function (request) { - - request.session.test = longstr; - request.reply('ok'); - } - } - }); - - server.route({ - method: 'GET', - path: '/get', - config: { - handler: function (request) { - - request.reply(request.session); - } - } - }); - - server.plugin.allow({ ext: true }).require('../../', options, function (err) { - - expect(err).to.not.exist; - server.inject({ method: 'GET', url: '/set' }, function (res) { - - expect(res.result).to.equal('ok'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - - var cookie = header[0].match(/(yar=[^\x00-\x20\"\,\;\\\x7F]*)/); - - server.inject({ method: 'GET', url: '/get', headers: { cookie: cookie[1] } }, function (res) { - - // expect(res.result).to.contain('test'); - var header = res.headers['set-cookie']; - expect(header.length).to.equal(1); - done(); - }); - }); - }); - }); - }); -}); \ No newline at end of file