index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /**
  2. * Module dependencies.
  3. */
  4. var debug = require('debug')('cookie-session');
  5. var Cookies = require('cookies');
  6. /**
  7. * Initialize session middleware with options.
  8. *
  9. * See README.md for documentation of options.
  10. *
  11. * @param {Object} [opts]
  12. * @return {Function} middleware
  13. * @api public
  14. */
  15. module.exports = function(opts){
  16. opts = opts || {};
  17. // name - previously "opts.key"
  18. var name = opts.name || opts.key || 'express:sess';
  19. // secrets
  20. var keys = opts.keys;
  21. if (!keys && opts.secret) keys = [opts.secret];
  22. // defaults
  23. if (null == opts.overwrite) opts.overwrite = true;
  24. if (null == opts.httpOnly) opts.httpOnly = true;
  25. if (null == opts.signed) opts.signed = true;
  26. if (!keys && opts.signed) throw new Error('.keys required.');
  27. debug('session options %j', opts);
  28. return function cookieSession(req, res, next){
  29. var cookies = req.sessionCookies = new Cookies(req, res, keys);
  30. var sess, json;
  31. // to pass to Session()
  32. req.sessionOptions = opts;
  33. req.sessionKey = name;
  34. req.__defineGetter__('session', function(){
  35. // already retrieved
  36. if (sess) return sess;
  37. // unset
  38. if (false === sess) return null;
  39. json = cookies.get(name, opts);
  40. if (json) {
  41. debug('parse %s', json);
  42. try {
  43. sess = new Session(req, decode(json));
  44. } catch (err) {
  45. // backwards compatibility:
  46. // create a new session if parsing fails.
  47. // new Buffer(string, 'base64') does not seem to crash
  48. // when `string` is not base64-encoded.
  49. // but `JSON.parse(string)` will crash.
  50. if (!(err instanceof SyntaxError)) throw err;
  51. sess = new Session(req);
  52. }
  53. } else {
  54. debug('new session');
  55. sess = new Session(req);
  56. }
  57. return sess;
  58. });
  59. req.__defineSetter__('session', function(val){
  60. if (null == val) return sess = false;
  61. if ('object' == typeof val) return sess = new Session(req, val);
  62. throw new Error('req.session can only be set as null or an object.');
  63. });
  64. var writeHead = res.writeHead;
  65. res.writeHead = function () {
  66. if (undefined === sess) {
  67. // not accessed
  68. } else if (false === sess) {
  69. // remove
  70. cookies.set(name, '', opts);
  71. } else if (!json && !sess.length) {
  72. // do nothing if new and not populated
  73. } else if (sess.changed(json)) {
  74. // save
  75. sess.save();
  76. }
  77. writeHead.apply(res, arguments);
  78. }
  79. next();
  80. }
  81. };
  82. /**
  83. * Session model.
  84. *
  85. * @param {Context} ctx
  86. * @param {Object} obj
  87. * @api private
  88. */
  89. function Session(ctx, obj) {
  90. this._ctx = ctx;
  91. if (!obj) this.isNew = true;
  92. else for (var k in obj) this[k] = obj[k];
  93. }
  94. /**
  95. * JSON representation of the session.
  96. *
  97. * @return {Object}
  98. * @api public
  99. */
  100. Session.prototype.inspect =
  101. Session.prototype.toJSON = function(){
  102. var self = this;
  103. var obj = {};
  104. Object.keys(this).forEach(function(key){
  105. if ('isNew' == key) return;
  106. if ('_' == key[0]) return;
  107. obj[key] = self[key];
  108. });
  109. return obj;
  110. };
  111. /**
  112. * Check if the session has changed relative to the `prev`
  113. * JSON value from the request.
  114. *
  115. * @param {String} [prev]
  116. * @return {Boolean}
  117. * @api private
  118. */
  119. Session.prototype.changed = function(prev){
  120. if (!prev) return true;
  121. this._json = encode(this);
  122. return this._json != prev;
  123. };
  124. /**
  125. * Return how many values there are in the session object.
  126. * Used to see if it's "populated".
  127. *
  128. * @return {Number}
  129. * @api public
  130. */
  131. Session.prototype.__defineGetter__('length', function(){
  132. return Object.keys(this.toJSON()).length;
  133. });
  134. /**
  135. * populated flag, which is just a boolean alias of .length.
  136. *
  137. * @return {Boolean}
  138. * @api public
  139. */
  140. Session.prototype.__defineGetter__('populated', function(){
  141. return !!this.length;
  142. });
  143. /**
  144. * Save session changes by
  145. * performing a Set-Cookie.
  146. *
  147. * @api private
  148. */
  149. Session.prototype.save = function(){
  150. var ctx = this._ctx;
  151. var json = this._json || encode(this);
  152. var opts = ctx.sessionOptions;
  153. var name = ctx.sessionKey;
  154. debug('save %s', json);
  155. ctx.sessionCookies.set(name, json, opts);
  156. };
  157. /**
  158. * Decode the base64 cookie value to an object.
  159. *
  160. * @param {String} string
  161. * @return {Object}
  162. * @api private
  163. */
  164. function decode(string) {
  165. var body = new Buffer(string, 'base64').toString('utf8');
  166. return JSON.parse(body);
  167. }
  168. /**
  169. * Encode an object into a base64-encoded JSON string.
  170. *
  171. * @param {Object} body
  172. * @return {String}
  173. * @api private
  174. */
  175. function encode(body) {
  176. body = JSON.stringify(body);
  177. return new Buffer(body).toString('base64');
  178. }