index.js 4.8 KB

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