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.
346 lines
9.3 KiB
346 lines
9.3 KiB
|
|
/*! |
|
* Connect - session |
|
* Copyright(c) 2010 Sencha Inc. |
|
* Copyright(c) 2011 TJ Holowaychuk |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var Session = require('./session/session') |
|
, MemoryStore = require('./session/memory') |
|
, Cookie = require('./session/cookie') |
|
, Store = require('./session/store') |
|
, utils = require('./../utils') |
|
, parse = require('url').parse |
|
, crypto = require('crypto'); |
|
|
|
// environment |
|
|
|
var env = process.env.NODE_ENV; |
|
|
|
/** |
|
* Expose the middleware. |
|
*/ |
|
|
|
exports = module.exports = session; |
|
|
|
/** |
|
* Expose constructors. |
|
*/ |
|
|
|
exports.Store = Store; |
|
exports.Cookie = Cookie; |
|
exports.Session = Session; |
|
exports.MemoryStore = MemoryStore; |
|
|
|
/** |
|
* Warning message for `MemoryStore` usage in production. |
|
*/ |
|
|
|
var warning = 'Warning: connection.session() MemoryStore is not\n' |
|
+ 'designed for a production environment, as it will leak\n' |
|
+ 'memory, and obviously only work within a single process.'; |
|
|
|
/** |
|
* Default finger-printing function. |
|
*/ |
|
|
|
function defaultFingerprint(req) { |
|
var ua = req.headers['user-agent'] || ''; |
|
return ua.replace(/;?\schromeframe\/[\d\.]+/, ''); |
|
}; |
|
|
|
/** |
|
* Paths to ignore. |
|
*/ |
|
|
|
exports.ignore = []; |
|
|
|
/** |
|
* Setup session store with the given `options`. |
|
* |
|
* Session data is _not_ saved in the cookie itself, however |
|
* cookies are used, so we must use the [cookieParser()](middleware-cookieParser.html) |
|
* middleware _before_ `session()`. |
|
* |
|
* Examples: |
|
* |
|
* connect.createServer( |
|
* connect.cookieParser() |
|
* , connect.session({ secret: 'keyboard cat' }) |
|
* ); |
|
* |
|
* Options: |
|
* |
|
* - `key` cookie name defaulting to `connect.sid` |
|
* - `store` Session store instance |
|
* - `fingerprint` Custom fingerprint generating function |
|
* - `cookie` Session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: 14400000 }` |
|
* - `secret` Secret string used to compute hash |
|
* |
|
* Ignore Paths: |
|
* |
|
* By default `/favicon.ico` is the only ignored path, all others |
|
* will utilize sessions, to manipulate the paths ignored, use |
|
* `connect.session.ignore.push('/my/path')`. This works for _full_ |
|
* pathnames only, not segments nor substrings. |
|
* |
|
* connect.session.ignore.push('/robots.txt'); |
|
* |
|
* ## req.session |
|
* |
|
* To store or access session data, simply use the request property `req.session`, |
|
* which is (generally) serialized as JSON by the store, so nested objects |
|
* are typically fine. For example below is a user-specific view counter: |
|
* |
|
* connect( |
|
* connect.cookieParser() |
|
* , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}) |
|
* , connect.favicon() |
|
* , function(req, res, next){ |
|
* var sess = req.session; |
|
* if (sess.views) { |
|
* res.setHeader('Content-Type', 'text/html'); |
|
* res.write('<p>views: ' + sess.views + '</p>'); |
|
* res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>'); |
|
* res.end(); |
|
* sess.views++; |
|
* } else { |
|
* sess.views = 1; |
|
* res.end('welcome to the session demo. refresh!'); |
|
* } |
|
* } |
|
* ).listen(3000); |
|
* |
|
* ## Session#regenerate() |
|
* |
|
* To regenerate the session simply invoke the method, once complete |
|
* a new SID and `Session` instance will be initialized at `req.session`. |
|
* |
|
* req.session.regenerate(function(err){ |
|
* // will have a new session here |
|
* }); |
|
* |
|
* ## Session#destroy() |
|
* |
|
* Destroys the session, removing `req.session`, will be re-generated next request. |
|
* |
|
* req.session.destroy(function(err){ |
|
* // cannot access session here |
|
* }); |
|
* |
|
* ## Session#reload() |
|
* |
|
* Reloads the session data. |
|
* |
|
* req.session.reload(function(err){ |
|
* // session updated |
|
* }); |
|
* |
|
* ## Session#save() |
|
* |
|
* Save the session. |
|
* |
|
* req.session.save(function(err){ |
|
* // session saved |
|
* }); |
|
* |
|
* ## Session#touch() |
|
* |
|
* Updates the `.maxAge`, and `.lastAccess` properties. Typically this is |
|
* not necessary to call, as the session middleware does this for you. |
|
* |
|
* ## Session#cookie |
|
* |
|
* Each session has a unique cookie object accompany it. This allows |
|
* you to alter the session cookie per visitor. For example we can |
|
* set `req.session.cookie.expires` to `false` to enable the cookie |
|
* to remain for only the duration of the user-agent. |
|
* |
|
* ## Session#maxAge |
|
* |
|
* Alternatively `req.session.cookie.maxAge` will return the time |
|
* remaining in milliseconds, which we may also re-assign a new value |
|
* to adjust the `.expires` property appropriately. The following |
|
* are essentially equivalent |
|
* |
|
* var hour = 3600000; |
|
* req.session.cookie.expires = new Date(Date.now() + hour); |
|
* req.session.cookie.maxAge = hour; |
|
* |
|
* For example when `maxAge` is set to `60000` (one minute), and 30 seconds |
|
* has elapsed it will return `30000` until the current request has completed, |
|
* at which time `req.session.touch()` is called to update `req.session.lastAccess`, |
|
* and reset `req.session.maxAge` to its original value. |
|
* |
|
* req.session.cookie.maxAge; |
|
* // => 30000 |
|
* |
|
* Session Store Implementation: |
|
* |
|
* Every session store _must_ implement the following methods |
|
* |
|
* - `.get(sid, callback)` |
|
* - `.set(sid, session, callback)` |
|
* - `.destroy(sid, callback)` |
|
* |
|
* Recommended methods include, but are not limited to: |
|
* |
|
* - `.length(callback)` |
|
* - `.clear(callback)` |
|
* |
|
* For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. |
|
* |
|
* @param {Object} options |
|
* @return {Function} |
|
* @api public |
|
*/ |
|
|
|
function session(options){ |
|
var options = options || {} |
|
, key = options.key || 'connect.sid' |
|
, secret = options.secret |
|
, store = options.store || new MemoryStore |
|
, fingerprint = options.fingerprint || defaultFingerprint |
|
, cookie = options.cookie; |
|
|
|
// notify user that this store is not |
|
// meant for a production environment |
|
if ('production' == env && store instanceof MemoryStore) { |
|
console.warn(warning); |
|
} |
|
|
|
// ensure secret is present |
|
if (!secret) { |
|
throw new Error('connect.session({ secret: "string" }) required for security'); |
|
} |
|
|
|
// session hashing function |
|
store.hash = function(req, base) { |
|
return crypto |
|
.createHmac('sha256', secret) |
|
.update(base + fingerprint(req)) |
|
.digest('base64') |
|
.replace(/=*$/, ''); |
|
}; |
|
|
|
// generates the new session |
|
store.generate = function(req){ |
|
var base = utils.uid(24); |
|
var sessionID = base + '.' + store.hash(req, base); |
|
req.sessionID = sessionID; |
|
req.session = new Session(req); |
|
req.session.cookie = new Cookie(cookie); |
|
}; |
|
|
|
return function session(req, res, next) { |
|
// self-awareness |
|
if (req.session) return next(); |
|
|
|
// parse url |
|
var url = parse(req.url) |
|
, path = url.pathname; |
|
|
|
// ignorable paths |
|
if (~exports.ignore.indexOf(path)) return next(); |
|
|
|
// expose store |
|
req.sessionStore = store; |
|
|
|
// proxy writeHead() to Set-Cookie |
|
var writeHead = res.writeHead; |
|
res.writeHead = function(status, headers){ |
|
if (req.session) { |
|
var cookie = req.session.cookie; |
|
// only send secure session cookies when there is a secure connection. |
|
// proxySecure is a custom attribute to allow for a reverse proxy |
|
// to handle SSL connections and to communicate to connect over HTTP that |
|
// the incoming connection is secure. |
|
var secured = cookie.secure && (req.connection.encrypted || req.connection.proxySecure); |
|
if (secured || !cookie.secure) { |
|
res.setHeader('Set-Cookie', cookie.serialize(key, req.sessionID)); |
|
} |
|
} |
|
|
|
res.writeHead = writeHead; |
|
return res.writeHead(status, headers); |
|
}; |
|
|
|
// proxy end() to commit the session |
|
var end = res.end; |
|
res.end = function(data, encoding){ |
|
res.end = end; |
|
if (req.session) { |
|
// HACK: ensure Set-Cookie for implicit writeHead() |
|
if (!res._header) res._implicitHeader(); |
|
req.session.resetMaxAge(); |
|
req.session.save(function(){ |
|
res.end(data, encoding); |
|
}); |
|
} else { |
|
res.end(data, encoding); |
|
} |
|
}; |
|
|
|
// session hashing |
|
function hash(base) { |
|
return store.hash(req, base); |
|
} |
|
|
|
// generate the session |
|
function generate() { |
|
store.generate(req); |
|
} |
|
|
|
// get the sessionID from the cookie |
|
req.sessionID = req.cookies[key]; |
|
|
|
// make a new session if the browser doesn't send a sessionID |
|
if (!req.sessionID) { |
|
generate(); |
|
next(); |
|
return; |
|
} |
|
|
|
// check the fingerprint |
|
var parts = req.sessionID.split('.'); |
|
if (parts[1] != hash(parts[0])) { |
|
generate(); |
|
next(); |
|
return; |
|
} |
|
|
|
// generate the session object |
|
var pause = utils.pause(req); |
|
store.get(req.sessionID, function(err, sess){ |
|
// proxy to resume() events |
|
var _next = next; |
|
next = function(err){ |
|
_next(err); |
|
pause.resume(); |
|
} |
|
|
|
// error handling |
|
if (err) { |
|
if ('ENOENT' == err.code) { |
|
generate(); |
|
next(); |
|
} else { |
|
next(err); |
|
} |
|
// no session |
|
} else if (!sess) { |
|
generate(); |
|
next(); |
|
// populate req.session |
|
} else { |
|
store.createSession(req, sess); |
|
next(); |
|
} |
|
}); |
|
}; |
|
};
|
|
|