프로젝트

일반

사용자정보

통계
| 개정판:

root / HServer / 00.Server / 00.Program / node_modules / express-session / index.js

이력 | 보기 | 이력해설 | 다운로드 (15.4 KB)

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