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