root / HServer / 00.Server / 00.Program / node_modules / express-session / index.js
이력 | 보기 | 이력해설 | 다운로드 (15.4 KB)
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 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 |
} |