index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. /*!
  2. * express-session
  3. * Copyright(c) 2010 Sencha Inc.
  4. * Copyright(c) 2011 TJ Holowaychuk
  5. * Copyright(c) 2014-2015 Douglas Christopher Wilson
  6. * MIT Licensed
  7. */
  8. 'use strict';
  9. /**
  10. * Module dependencies.
  11. * @private
  12. */
  13. var Buffer = require('safe-buffer').Buffer
  14. var cookie = require('cookie');
  15. var crypto = require('crypto')
  16. var debug = require('debug')('express-session');
  17. var deprecate = require('depd')('express-session');
  18. var onHeaders = require('on-headers')
  19. var parseUrl = require('parseurl');
  20. var signature = require('cookie-signature')
  21. var uid = require('uid-safe').sync
  22. var Cookie = require('./session/cookie')
  23. var MemoryStore = require('./session/memory')
  24. var Session = require('./session/session')
  25. var Store = require('./session/store')
  26. // environment
  27. var env = process.env.NODE_ENV;
  28. /**
  29. * Expose the middleware.
  30. */
  31. exports = module.exports = session;
  32. /**
  33. * Expose constructors.
  34. */
  35. exports.Store = Store;
  36. exports.Cookie = Cookie;
  37. exports.Session = Session;
  38. exports.MemoryStore = MemoryStore;
  39. /**
  40. * Warning message for `MemoryStore` usage in production.
  41. * @private
  42. */
  43. var warning = 'Warning: connect.session() MemoryStore is not\n'
  44. + 'designed for a production environment, as it will leak\n'
  45. + 'memory, and will not scale past a single process.';
  46. /**
  47. * Node.js 0.8+ async implementation.
  48. * @private
  49. */
  50. /* istanbul ignore next */
  51. var defer = typeof setImmediate === 'function'
  52. ? setImmediate
  53. : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
  54. /**
  55. * Setup session store with the given `options`.
  56. *
  57. * @param {Object} [options]
  58. * @param {Object} [options.cookie] Options for cookie
  59. * @param {Function} [options.genid]
  60. * @param {String} [options.name=connect.sid] Session ID cookie name
  61. * @param {Boolean} [options.proxy]
  62. * @param {Boolean} [options.resave] Resave unmodified sessions back to the store
  63. * @param {Boolean} [options.rolling] Enable/disable rolling session expiration
  64. * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
  65. * @param {String|Array} [options.secret] Secret for signing session ID
  66. * @param {Object} [options.store=MemoryStore] Session store
  67. * @param {String} [options.unset]
  68. * @return {Function} middleware
  69. * @public
  70. */
  71. function session(options) {
  72. var opts = options || {}
  73. // get the cookie options
  74. var cookieOptions = opts.cookie || {}
  75. // get the session id generate function
  76. var generateId = opts.genid || generateSessionId
  77. // get the session cookie name
  78. var name = opts.name || opts.key || 'connect.sid'
  79. // get the session store
  80. var store = opts.store || new MemoryStore()
  81. // get the trust proxy setting
  82. var trustProxy = opts.proxy
  83. // get the resave session option
  84. var resaveSession = opts.resave;
  85. // get the rolling session option
  86. var rollingSessions = Boolean(opts.rolling)
  87. // get the save uninitialized session option
  88. var saveUninitializedSession = opts.saveUninitialized
  89. // get the cookie signing secret
  90. var secret = opts.secret
  91. if (typeof generateId !== 'function') {
  92. throw new TypeError('genid option must be a function');
  93. }
  94. if (resaveSession === undefined) {
  95. deprecate('undefined resave option; provide resave option');
  96. resaveSession = true;
  97. }
  98. if (saveUninitializedSession === undefined) {
  99. deprecate('undefined saveUninitialized option; provide saveUninitialized option');
  100. saveUninitializedSession = true;
  101. }
  102. if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') {
  103. throw new TypeError('unset option must be "destroy" or "keep"');
  104. }
  105. // TODO: switch to "destroy" on next major
  106. var unsetDestroy = opts.unset === 'destroy'
  107. if (Array.isArray(secret) && secret.length === 0) {
  108. throw new TypeError('secret option array must contain one or more strings');
  109. }
  110. if (secret && !Array.isArray(secret)) {
  111. secret = [secret];
  112. }
  113. if (!secret) {
  114. deprecate('req.secret; provide secret option');
  115. }
  116. // notify user that this store is not
  117. // meant for a production environment
  118. /* istanbul ignore next: not tested */
  119. if (env === 'production' && store instanceof MemoryStore) {
  120. console.warn(warning);
  121. }
  122. // generates the new session
  123. store.generate = function(req){
  124. req.sessionID = generateId(req);
  125. req.session = new Session(req);
  126. req.session.cookie = new Cookie(cookieOptions);
  127. if (cookieOptions.secure === 'auto') {
  128. req.session.cookie.secure = issecure(req, trustProxy);
  129. }
  130. };
  131. var storeImplementsTouch = typeof store.touch === 'function';
  132. // register event listeners for the store to track readiness
  133. var storeReady = true
  134. store.on('disconnect', function ondisconnect() {
  135. storeReady = false
  136. })
  137. store.on('connect', function onconnect() {
  138. storeReady = true
  139. })
  140. return function session(req, res, next) {
  141. // self-awareness
  142. if (req.session) {
  143. next()
  144. return
  145. }
  146. // Handle connection as if there is no session if
  147. // the store has temporarily disconnected etc
  148. if (!storeReady) {
  149. debug('store is disconnected')
  150. next()
  151. return
  152. }
  153. // pathname mismatch
  154. var originalPath = parseUrl.original(req).pathname || '/'
  155. if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next();
  156. // ensure a secret is available or bail
  157. if (!secret && !req.secret) {
  158. next(new Error('secret option required for sessions'));
  159. return;
  160. }
  161. // backwards compatibility for signed cookies
  162. // req.secret is passed from the cookie parser middleware
  163. var secrets = secret || [req.secret];
  164. var originalHash;
  165. var originalId;
  166. var savedHash;
  167. var touched = false
  168. // expose store
  169. req.sessionStore = store;
  170. // get the session ID from the cookie
  171. var cookieId = req.sessionID = getcookie(req, name, secrets);
  172. // set-cookie
  173. onHeaders(res, function(){
  174. if (!req.session) {
  175. debug('no session');
  176. return;
  177. }
  178. if (!shouldSetCookie(req)) {
  179. return;
  180. }
  181. // only send secure cookies via https
  182. if (req.session.cookie.secure && !issecure(req, trustProxy)) {
  183. debug('not secured');
  184. return;
  185. }
  186. if (!touched) {
  187. // touch session
  188. req.session.touch()
  189. touched = true
  190. }
  191. // set cookie
  192. setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
  193. });
  194. // proxy end() to commit the session
  195. var _end = res.end;
  196. var _write = res.write;
  197. var ended = false;
  198. res.end = function end(chunk, encoding) {
  199. if (ended) {
  200. return false;
  201. }
  202. ended = true;
  203. var ret;
  204. var sync = true;
  205. function writeend() {
  206. if (sync) {
  207. ret = _end.call(res, chunk, encoding);
  208. sync = false;
  209. return;
  210. }
  211. _end.call(res);
  212. }
  213. function writetop() {
  214. if (!sync) {
  215. return ret;
  216. }
  217. if (chunk == null) {
  218. ret = true;
  219. return ret;
  220. }
  221. var contentLength = Number(res.getHeader('Content-Length'));
  222. if (!isNaN(contentLength) && contentLength > 0) {
  223. // measure chunk
  224. chunk = !Buffer.isBuffer(chunk)
  225. ? Buffer.from(chunk, encoding)
  226. : chunk;
  227. encoding = undefined;
  228. if (chunk.length !== 0) {
  229. debug('split response');
  230. ret = _write.call(res, chunk.slice(0, chunk.length - 1));
  231. chunk = chunk.slice(chunk.length - 1, chunk.length);
  232. return ret;
  233. }
  234. }
  235. ret = _write.call(res, chunk, encoding);
  236. sync = false;
  237. return ret;
  238. }
  239. if (shouldDestroy(req)) {
  240. // destroy session
  241. debug('destroying');
  242. store.destroy(req.sessionID, function ondestroy(err) {
  243. if (err) {
  244. defer(next, err);
  245. }
  246. debug('destroyed');
  247. writeend();
  248. });
  249. return writetop();
  250. }
  251. // no session to save
  252. if (!req.session) {
  253. debug('no session');
  254. return _end.call(res, chunk, encoding);
  255. }
  256. if (!touched) {
  257. // touch session
  258. req.session.touch()
  259. touched = true
  260. }
  261. if (shouldSave(req)) {
  262. req.session.save(function onsave(err) {
  263. if (err) {
  264. defer(next, err);
  265. }
  266. writeend();
  267. });
  268. return writetop();
  269. } else if (storeImplementsTouch && shouldTouch(req)) {
  270. // store implements touch method
  271. debug('touching');
  272. store.touch(req.sessionID, req.session, function ontouch(err) {
  273. if (err) {
  274. defer(next, err);
  275. }
  276. debug('touched');
  277. writeend();
  278. });
  279. return writetop();
  280. }
  281. return _end.call(res, chunk, encoding);
  282. };
  283. // generate the session
  284. function generate() {
  285. store.generate(req);
  286. originalId = req.sessionID;
  287. originalHash = hash(req.session);
  288. wrapmethods(req.session);
  289. }
  290. // inflate the session
  291. function inflate (req, sess) {
  292. store.createSession(req, sess)
  293. originalId = req.sessionID
  294. originalHash = hash(sess)
  295. if (!resaveSession) {
  296. savedHash = originalHash
  297. }
  298. wrapmethods(req.session)
  299. }
  300. // wrap session methods
  301. function wrapmethods(sess) {
  302. var _reload = sess.reload
  303. var _save = sess.save;
  304. function reload(callback) {
  305. debug('reloading %s', this.id)
  306. _reload.call(this, function () {
  307. wrapmethods(req.session)
  308. callback.apply(this, arguments)
  309. })
  310. }
  311. function save() {
  312. debug('saving %s', this.id);
  313. savedHash = hash(this);
  314. _save.apply(this, arguments);
  315. }
  316. Object.defineProperty(sess, 'reload', {
  317. configurable: true,
  318. enumerable: false,
  319. value: reload,
  320. writable: true
  321. })
  322. Object.defineProperty(sess, 'save', {
  323. configurable: true,
  324. enumerable: false,
  325. value: save,
  326. writable: true
  327. });
  328. }
  329. // check if session has been modified
  330. function isModified(sess) {
  331. return originalId !== sess.id || originalHash !== hash(sess);
  332. }
  333. // check if session has been saved
  334. function isSaved(sess) {
  335. return originalId === sess.id && savedHash === hash(sess);
  336. }
  337. // determine if session should be destroyed
  338. function shouldDestroy(req) {
  339. return req.sessionID && unsetDestroy && req.session == null;
  340. }
  341. // determine if session should be saved to store
  342. function shouldSave(req) {
  343. // cannot set cookie without a session ID
  344. if (typeof req.sessionID !== 'string') {
  345. debug('session ignored because of bogus req.sessionID %o', req.sessionID);
  346. return false;
  347. }
  348. return !saveUninitializedSession && cookieId !== req.sessionID
  349. ? isModified(req.session)
  350. : !isSaved(req.session)
  351. }
  352. // determine if session should be touched
  353. function shouldTouch(req) {
  354. // cannot set cookie without a session ID
  355. if (typeof req.sessionID !== 'string') {
  356. debug('session ignored because of bogus req.sessionID %o', req.sessionID);
  357. return false;
  358. }
  359. return cookieId === req.sessionID && !shouldSave(req);
  360. }
  361. // determine if cookie should be set on response
  362. function shouldSetCookie(req) {
  363. // cannot set cookie without a session ID
  364. if (typeof req.sessionID !== 'string') {
  365. return false;
  366. }
  367. return cookieId !== req.sessionID
  368. ? saveUninitializedSession || isModified(req.session)
  369. : rollingSessions || req.session.cookie.expires != null && isModified(req.session);
  370. }
  371. // generate a session if the browser doesn't send a sessionID
  372. if (!req.sessionID) {
  373. debug('no SID sent, generating session');
  374. generate();
  375. next();
  376. return;
  377. }
  378. // generate the session object
  379. debug('fetching %s', req.sessionID);
  380. store.get(req.sessionID, function(err, sess){
  381. // error handling
  382. if (err && err.code !== 'ENOENT') {
  383. debug('error %j', err);
  384. next(err)
  385. return
  386. }
  387. try {
  388. if (err || !sess) {
  389. debug('no session found')
  390. generate()
  391. } else {
  392. debug('session found')
  393. inflate(req, sess)
  394. }
  395. } catch (e) {
  396. next(e)
  397. return
  398. }
  399. next()
  400. });
  401. };
  402. };
  403. /**
  404. * Generate a session ID for a new session.
  405. *
  406. * @return {String}
  407. * @private
  408. */
  409. function generateSessionId(sess) {
  410. return uid(24);
  411. }
  412. /**
  413. * Get the session ID cookie from request.
  414. *
  415. * @return {string}
  416. * @private
  417. */
  418. function getcookie(req, name, secrets) {
  419. var header = req.headers.cookie;
  420. var raw;
  421. var val;
  422. // read from cookie header
  423. if (header) {
  424. var cookies = cookie.parse(header);
  425. raw = cookies[name];
  426. if (raw) {
  427. if (raw.substr(0, 2) === 's:') {
  428. val = unsigncookie(raw.slice(2), secrets);
  429. if (val === false) {
  430. debug('cookie signature invalid');
  431. val = undefined;
  432. }
  433. } else {
  434. debug('cookie unsigned')
  435. }
  436. }
  437. }
  438. // back-compat read from cookieParser() signedCookies data
  439. if (!val && req.signedCookies) {
  440. val = req.signedCookies[name];
  441. if (val) {
  442. deprecate('cookie should be available in req.headers.cookie');
  443. }
  444. }
  445. // back-compat read from cookieParser() cookies data
  446. if (!val && req.cookies) {
  447. raw = req.cookies[name];
  448. if (raw) {
  449. if (raw.substr(0, 2) === 's:') {
  450. val = unsigncookie(raw.slice(2), secrets);
  451. if (val) {
  452. deprecate('cookie should be available in req.headers.cookie');
  453. }
  454. if (val === false) {
  455. debug('cookie signature invalid');
  456. val = undefined;
  457. }
  458. } else {
  459. debug('cookie unsigned')
  460. }
  461. }
  462. }
  463. return val;
  464. }
  465. /**
  466. * Hash the given `sess` object omitting changes to `.cookie`.
  467. *
  468. * @param {Object} sess
  469. * @return {String}
  470. * @private
  471. */
  472. function hash(sess) {
  473. // serialize
  474. var str = JSON.stringify(sess, function (key, val) {
  475. // ignore sess.cookie property
  476. if (this === sess && key === 'cookie') {
  477. return
  478. }
  479. return val
  480. })
  481. // hash
  482. return crypto
  483. .createHash('sha1')
  484. .update(str, 'utf8')
  485. .digest('hex')
  486. }
  487. /**
  488. * Determine if request is secure.
  489. *
  490. * @param {Object} req
  491. * @param {Boolean} [trustProxy]
  492. * @return {Boolean}
  493. * @private
  494. */
  495. function issecure(req, trustProxy) {
  496. // socket is https server
  497. if (req.connection && req.connection.encrypted) {
  498. return true;
  499. }
  500. // do not trust proxy
  501. if (trustProxy === false) {
  502. return false;
  503. }
  504. // no explicit trust; try req.secure from express
  505. if (trustProxy !== true) {
  506. return req.secure === true
  507. }
  508. // read the proto from x-forwarded-proto header
  509. var header = req.headers['x-forwarded-proto'] || '';
  510. var index = header.indexOf(',');
  511. var proto = index !== -1
  512. ? header.substr(0, index).toLowerCase().trim()
  513. : header.toLowerCase().trim()
  514. return proto === 'https';
  515. }
  516. /**
  517. * Set cookie on response.
  518. *
  519. * @private
  520. */
  521. function setcookie(res, name, val, secret, options) {
  522. var signed = 's:' + signature.sign(val, secret);
  523. var data = cookie.serialize(name, signed, options);
  524. debug('set-cookie %s', data);
  525. var prev = res.getHeader('Set-Cookie') || []
  526. var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
  527. res.setHeader('Set-Cookie', header)
  528. }
  529. /**
  530. * Verify and decode the given `val` with `secrets`.
  531. *
  532. * @param {String} val
  533. * @param {Array} secrets
  534. * @returns {String|Boolean}
  535. * @private
  536. */
  537. function unsigncookie(val, secrets) {
  538. for (var i = 0; i < secrets.length; i++) {
  539. var result = signature.unsign(val, secrets[i]);
  540. if (result !== false) {
  541. return result;
  542. }
  543. }
  544. return false;
  545. }