You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
10 KiB

  1. /*!
  2. * Connect - session
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * MIT Licensed
  6. */
  7. /**
  8. * Module dependencies.
  9. */
  10. var Session = require('./session/session')
  11. , debug = require('debug')('connect:session')
  12. , MemoryStore = require('./session/memory')
  13. , signature = require('cookie-signature')
  14. , Cookie = require('./session/cookie')
  15. , Store = require('./session/store')
  16. , utils = require('./../utils')
  17. , parse = utils.parseUrl
  18. , crc32 = require('buffer-crc32');
  19. // environment
  20. var env = process.env.NODE_ENV;
  21. /**
  22. * Expose the middleware.
  23. */
  24. exports = module.exports = session;
  25. /**
  26. * Expose constructors.
  27. */
  28. exports.Store = Store;
  29. exports.Cookie = Cookie;
  30. exports.Session = Session;
  31. exports.MemoryStore = MemoryStore;
  32. /**
  33. * Warning message for `MemoryStore` usage in production.
  34. */
  35. var warning = 'Warning: connection.session() MemoryStore is not\n'
  36. + 'designed for a production environment, as it will leak\n'
  37. + 'memory, and will not scale past a single process.';
  38. /**
  39. * Session:
  40. *
  41. * Setup session store with the given `options`.
  42. *
  43. * Session data is _not_ saved in the cookie itself, however
  44. * cookies are used, so we must use the [cookieParser()](cookieParser.html)
  45. * middleware _before_ `session()`.
  46. *
  47. * Examples:
  48. *
  49. * connect()
  50. * .use(connect.cookieParser())
  51. * .use(connect.session({ secret: 'keyboard cat', key: 'sid', cookie: { secure: true }}))
  52. *
  53. * Options:
  54. *
  55. * - `key` cookie name defaulting to `connect.sid`
  56. * - `store` session store instance
  57. * - `secret` session cookie is signed with this secret to prevent tampering
  58. * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }`
  59. * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto")
  60. *
  61. * Cookie option:
  62. *
  63. * By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set
  64. * so the cookie becomes a browser-session cookie. When the user closes the
  65. * browser the cookie (and session) will be removed.
  66. *
  67. * ## req.session
  68. *
  69. * To store or access session data, simply use the request property `req.session`,
  70. * which is (generally) serialized as JSON by the store, so nested objects
  71. * are typically fine. For example below is a user-specific view counter:
  72. *
  73. * connect()
  74. * .use(connect.favicon())
  75. * .use(connect.cookieParser())
  76. * .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
  77. * .use(function(req, res, next){
  78. * var sess = req.session;
  79. * if (sess.views) {
  80. * res.setHeader('Content-Type', 'text/html');
  81. * res.write('<p>views: ' + sess.views + '</p>');
  82. * res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>');
  83. * res.end();
  84. * sess.views++;
  85. * } else {
  86. * sess.views = 1;
  87. * res.end('welcome to the session demo. refresh!');
  88. * }
  89. * }
  90. * )).listen(3000);
  91. *
  92. * ## Session#regenerate()
  93. *
  94. * To regenerate the session simply invoke the method, once complete
  95. * a new SID and `Session` instance will be initialized at `req.session`.
  96. *
  97. * req.session.regenerate(function(err){
  98. * // will have a new session here
  99. * });
  100. *
  101. * ## Session#destroy()
  102. *
  103. * Destroys the session, removing `req.session`, will be re-generated next request.
  104. *
  105. * req.session.destroy(function(err){
  106. * // cannot access session here
  107. * });
  108. *
  109. * ## Session#reload()
  110. *
  111. * Reloads the session data.
  112. *
  113. * req.session.reload(function(err){
  114. * // session updated
  115. * });
  116. *
  117. * ## Session#save()
  118. *
  119. * Save the session.
  120. *
  121. * req.session.save(function(err){
  122. * // session saved
  123. * });
  124. *
  125. * ## Session#touch()
  126. *
  127. * Updates the `.maxAge` property. Typically this is
  128. * not necessary to call, as the session middleware does this for you.
  129. *
  130. * ## Session#cookie
  131. *
  132. * Each session has a unique cookie object accompany it. This allows
  133. * you to alter the session cookie per visitor. For example we can
  134. * set `req.session.cookie.expires` to `false` to enable the cookie
  135. * to remain for only the duration of the user-agent.
  136. *
  137. * ## Session#maxAge
  138. *
  139. * Alternatively `req.session.cookie.maxAge` will return the time
  140. * remaining in milliseconds, which we may also re-assign a new value
  141. * to adjust the `.expires` property appropriately. The following
  142. * are essentially equivalent
  143. *
  144. * var hour = 3600000;
  145. * req.session.cookie.expires = new Date(Date.now() + hour);
  146. * req.session.cookie.maxAge = hour;
  147. *
  148. * For example when `maxAge` is set to `60000` (one minute), and 30 seconds
  149. * has elapsed it will return `30000` until the current request has completed,
  150. * at which time `req.session.touch()` is called to reset `req.session.maxAge`
  151. * to its original value.
  152. *
  153. * req.session.cookie.maxAge;
  154. * // => 30000
  155. *
  156. * Session Store Implementation:
  157. *
  158. * Every session store _must_ implement the following methods
  159. *
  160. * - `.get(sid, callback)`
  161. * - `.set(sid, session, callback)`
  162. * - `.destroy(sid, callback)`
  163. *
  164. * Recommended methods include, but are not limited to:
  165. *
  166. * - `.length(callback)`
  167. * - `.clear(callback)`
  168. *
  169. * For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo.
  170. *
  171. * @param {Object} options
  172. * @return {Function}
  173. * @api public
  174. */
  175. function session(options){
  176. var options = options || {}
  177. , key = options.key || 'connect.sid'
  178. , store = options.store || new MemoryStore
  179. , cookie = options.cookie || {}
  180. , trustProxy = options.proxy
  181. , storeReady = true;
  182. // notify user that this store is not
  183. // meant for a production environment
  184. if ('production' == env && store instanceof MemoryStore) {
  185. console.warn(warning);
  186. }
  187. // generates the new session
  188. store.generate = function(req){
  189. req.sessionID = utils.uid(24);
  190. req.session = new Session(req);
  191. req.session.cookie = new Cookie(cookie);
  192. };
  193. store.on('disconnect', function(){ storeReady = false; });
  194. store.on('connect', function(){ storeReady = true; });
  195. return function session(req, res, next) {
  196. // self-awareness
  197. if (req.session) return next();
  198. // Handle connection as if there is no session if
  199. // the store has temporarily disconnected etc
  200. if (!storeReady) return debug('store is disconnected'), next();
  201. // pathname mismatch
  202. if (0 != req.originalUrl.indexOf(cookie.path || '/')) return next();
  203. // backwards compatibility for signed cookies
  204. // req.secret is passed from the cookie parser middleware
  205. var secret = options.secret || req.secret;
  206. // ensure secret is available or bail
  207. if (!secret) throw new Error('`secret` option required for sessions');
  208. // parse url
  209. var originalHash
  210. , originalId;
  211. // expose store
  212. req.sessionStore = store;
  213. // grab the session cookie value and check the signature
  214. var rawCookie = req.cookies[key];
  215. // get signedCookies for backwards compat with signed cookies
  216. var unsignedCookie = req.signedCookies[key];
  217. if (!unsignedCookie && rawCookie) {
  218. unsignedCookie = utils.parseSignedCookie(rawCookie, secret);
  219. }
  220. // set-cookie
  221. res.on('header', function(){
  222. if (!req.session) return;
  223. var cookie = req.session.cookie
  224. , proto = (req.headers['x-forwarded-proto'] || '').split(',')[0].toLowerCase().trim()
  225. , tls = req.connection.encrypted || (trustProxy && 'https' == proto)
  226. , secured = cookie.secure && tls
  227. , isNew = unsignedCookie != req.sessionID;
  228. // only send secure cookies via https
  229. if (cookie.secure && !secured) return debug('not secured');
  230. // long expires, handle expiry server-side
  231. if (!isNew && cookie.hasLongExpires) return debug('already set cookie');
  232. // browser-session length cookie
  233. if (null == cookie.expires) {
  234. if (!isNew) return debug('already set browser-session cookie');
  235. // compare hashes and ids
  236. } else if (originalHash == hash(req.session) && originalId == req.session.id) {
  237. return debug('unmodified session');
  238. }
  239. var val = 's:' + signature.sign(req.sessionID, secret);
  240. val = cookie.serialize(key, val);
  241. debug('set-cookie %s', val);
  242. res.setHeader('Set-Cookie', val);
  243. });
  244. // proxy end() to commit the session
  245. var end = res.end;
  246. res.end = function(data, encoding){
  247. res.end = end;
  248. if (!req.session) return res.end(data, encoding);
  249. debug('saving');
  250. req.session.resetMaxAge();
  251. req.session.save(function(err){
  252. if (err) console.error(err.stack);
  253. debug('saved');
  254. res.end(data, encoding);
  255. });
  256. };
  257. // generate the session
  258. function generate() {
  259. store.generate(req);
  260. }
  261. // get the sessionID from the cookie
  262. req.sessionID = unsignedCookie;
  263. // generate a session if the browser doesn't send a sessionID
  264. if (!req.sessionID) {
  265. debug('no SID sent, generating session');
  266. generate();
  267. next();
  268. return;
  269. }
  270. // generate the session object
  271. var pause = utils.pause(req);
  272. debug('fetching %s', req.sessionID);
  273. store.get(req.sessionID, function(err, sess){
  274. // proxy to resume() events
  275. var _next = next;
  276. next = function(err){
  277. _next(err);
  278. pause.resume();
  279. };
  280. // error handling
  281. if (err) {
  282. debug('error %j', err);
  283. if ('ENOENT' == err.code) {
  284. generate();
  285. next();
  286. } else {
  287. next(err);
  288. }
  289. // no session
  290. } else if (!sess) {
  291. debug('no session found');
  292. generate();
  293. next();
  294. // populate req.session
  295. } else {
  296. debug('session found');
  297. store.createSession(req, sess);
  298. originalId = req.sessionID;
  299. originalHash = hash(sess);
  300. next();
  301. }
  302. });
  303. };
  304. };
  305. /**
  306. * Hash the given `sess` object omitting changes
  307. * to `.cookie`.
  308. *
  309. * @param {Object} sess
  310. * @return {String}
  311. * @api private
  312. */
  313. function hash(sess) {
  314. return crc32.signed(JSON.stringify(sess, function(key, val){
  315. if ('cookie' != key) return val;
  316. }));
  317. }