fruitstand
Version:
215 lines (178 loc) • 4.59 kB
JavaScript
/**
* Module dependencies.
*/
var debug = require('debug')('cookie-session');
var Cookies = require('cookies');
/**
* Initialize session middleware with `opts`:
*
* - `key` session cookie name ["koa:sess"]
* - all other options are passed as cookie options
*
* @param {Object} [opts]
* @api public
*/
module.exports = function(opts){
opts = opts || {};
// key
var key = opts.key || 'express:sess';
// secrets
var keys = opts.keys;
if (!keys && opts.secret) keys = [opts.secret];
// defaults
if (null == opts.overwrite) opts.overwrite = true;
if (null == opts.httpOnly) opts.httpOnly = true;
if (null == opts.signed) opts.signed = true;
if (!keys && opts.signed) throw new Error('.keys required.');
debug('session options %j', opts);
return function (req, res, next){
var cookies = req.sessionCookies = new Cookies(req, res, keys);
var sess, json;
// to pass to Session()
req.sessionOptions = opts;
req.sessionKey = key;
req.__defineGetter__('session', function(){
// already retrieved
if (sess) return sess;
// unset
if (false === sess) return null;
json = cookies.get(key, opts);
if (json) {
debug('parse %s', json);
try {
sess = new Session(req, decode(json));
} catch (err) {
// backwards compatibility:
// create a new session if parsing fails.
// new Buffer(string, 'base64') does not seem to crash
// when `string` is not base64-encoded.
// but `JSON.parse(string)` will crash.
if (!(err instanceof SyntaxError)) throw err;
sess = new Session(req);
}
} else {
debug('new session');
sess = new Session(req);
}
return sess;
});
req.__defineSetter__('session', function(val){
if (null == val) return sess = false;
if ('object' == typeof val) return sess = new Session(req, val);
throw new Error('req.session can only be set as null or an object.');
});
var writeHead = res.writeHead;
res.writeHead = function () {
if (undefined === sess) {
// not accessed
} else if (false === sess) {
// remove
cookies.set(key, '', opts);
} else if (!json && !sess.length) {
// do nothing if new and not populated
} else if (sess.changed(json)) {
// save
sess.save();
}
writeHead.apply(res, arguments);
}
next();
}
};
/**
* Session model.
*
* @param {Context} ctx
* @param {Object} obj
* @api private
*/
function Session(ctx, obj) {
this._ctx = ctx;
if (!obj) this.isNew = true;
else for (var k in obj) this[k] = obj[k];
}
/**
* JSON representation of the session.
*
* @return {Object}
* @api public
*/
Session.prototype.inspect =
Session.prototype.toJSON = function(){
var self = this;
var obj = {};
Object.keys(this).forEach(function(key){
if ('isNew' == key) return;
if ('_' == key[0]) return;
obj[key] = self[key];
});
return obj;
};
/**
* Check if the session has changed relative to the `prev`
* JSON value from the request.
*
* @param {String} [prev]
* @return {Boolean}
* @api private
*/
Session.prototype.changed = function(prev){
if (!prev) return true;
this._json = encode(this);
return this._json != prev;
};
/**
* Return how many values there are in the session object.
* Used to see if it's "populated".
*
* @return {Number}
* @api public
*/
Session.prototype.__defineGetter__('length', function(){
return Object.keys(this.toJSON()).length;
});
/**
* populated flag, which is just a boolean alias of .length.
*
* @return {Boolean}
* @api public
*/
Session.prototype.__defineGetter__('populated', function(){
return !!this.length;
});
/**
* Save session changes by
* performing a Set-Cookie.
*
* @api private
*/
Session.prototype.save = function(){
var ctx = this._ctx;
var json = this._json || encode(this);
var opts = ctx.sessionOptions;
var key = ctx.sessionKey;
debug('save %s', json);
ctx.sessionCookies.set(key, json, opts);
};
/**
* Decode the base64 cookie value to an object.
*
* @param {String} string
* @return {Object}
* @api private
*/
function decode(string) {
var body = new Buffer(string, 'base64').toString('utf8');
return JSON.parse(body);
}
/**
* Encode an object into a base64-encoded JSON string.
*
* @param {Object} body
* @return {String}
* @api private
*/
function encode(body) {
body = JSON.stringify(body);
return new Buffer(body).toString('base64');
}