4 * Copyright(c) 2010 Sencha Inc.
5 * Copyright(c) 2011 TJ Holowaychuk
10 * Module dependencies.
13 var Session = require('./session/session')
14 , MemoryStore = require('./session/memory')
15 , Cookie = require('./session/cookie')
16 , Store = require('./session/store')
17 , utils = require('./../utils')
18 , parse = require('url').parse
19 , crypto = require('crypto');
23 var env = process.env.NODE_ENV;
26 * Expose the middleware.
29 exports = module.exports = session;
32 * Expose constructors.
35 exports.Store = Store;
36 exports.Cookie = Cookie;
37 exports.Session = Session;
38 exports.MemoryStore = MemoryStore;
41 * Warning message for `MemoryStore` usage in production.
44 var warning = 'Warning: connection.session() MemoryStore is not\n'
45 + 'designed for a production environment, as it will leak\n'
46 + 'memory, and obviously only work within a single process.';
49 * Default finger-printing function.
52 function defaultFingerprint(req) {
53 return req.headers['user-agent'] || '';
57 * Paths to ignore, defaulting to `/favicon.ico`.
60 exports.ignore = ['/favicon.ico'];
63 * Setup session store with the given `options`.
65 * Session data is _not_ saved in the cookie itself, however
66 * cookies are used, so we must use the [cookieParser()](middleware-cookieParser.html)
67 * middleware _before_ `session()`.
71 * connect.createServer(
72 * connect.cookieParser()
73 * , connect.session({ secret: 'keyboard cat' })
78 * - `key` cookie name defaulting to `connect.sid`
79 * - `store` Session store instance
80 * - `fingerprint` Custom fingerprint generating function
81 * - `cookie` Session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: 14400000 }`
82 * - `secret` Secret string used to compute hash
86 * By default `/favicon.ico` is the only ignored path, all others
87 * will utilize sessions, to manipulate the paths ignored, use
88 * `connect.session.ignore.push('/my/path')`. This works for _full_
89 * pathnames only, not segments nor substrings.
91 * connect.session.ignore.push('/robots.txt');
95 * To store or access session data, simply use the request property `req.session`,
96 * which is (generally) serialized as JSON by the store, so nested objects
97 * are typically fine. For example below is a user-specific view counter:
100 * connect.cookieParser()
101 * , connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})
102 * , connect.favicon()
103 * , function(req, res, next){
104 * var sess = req.session;
106 * res.setHeader('Content-Type', 'text/html');
107 * res.write('<p>views: ' + sess.views + '</p>');
108 * res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>');
113 * res.end('welcome to the session demo. refresh!');
118 * ## Session#regenerate()
120 * To regenerate the session simply invoke the method, once complete
121 * a new SID and `Session` instance will be initialized at `req.session`.
123 * req.session.regenerate(function(err){
124 * // will have a new session here
127 * ## Session#destroy()
129 * Destroys the session, removing `req.session`, will be re-generated next request.
131 * req.session.destroy(function(err){
132 * // cannot access session here
137 * Updates the `.maxAge`, and `.lastAccess` properties. Typically this is
138 * not necessary to call, as the session middleware does this for you.
142 * Each session has a unique cookie object accompany it. This allows
143 * you to alter the session cookie per visitor. For example we can
144 * set `req.session.cookie.expires` to `false` to enable the cookie
145 * to remain for only the duration of the user-agent.
149 * Alternatively `req.session.cookie.maxAge` will return the time
150 * remaining in milliseconds, which we may also re-assign a new value
151 * to adjust the `.expires` property appropriately. The following
152 * are essentially equivalent
154 * var hour = 3600000;
155 * req.session.cookie.expires = new Date(Date.now() + hour);
156 * req.session.cookie.maxAge = hour;
158 * For example when `maxAge` is set to `60000` (one minute), and 30 seconds
159 * has elapsed it will return `30000` until the current request has completed,
160 * at which time `req.session.touch()` is called to update `req.session.lastAccess`,
161 * and reset `req.session.maxAge` to its original value.
163 * req.session.cookie.maxAge;
166 * Session Store Implementation:
168 * Every session store _must_ implement the following methods
170 * - `.get(sid, callback)`
171 * - `.set(sid, session, callback)`
172 * - `.destroy(sid, callback)`
174 * Recommended methods include, but are not limited to:
176 * - `.length(callback)`
177 * - `.clear(callback)`
179 * For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo.
181 * @param {Object} options
186 function session(options){
187 var options = options || {}
188 , key = options.key || 'connect.sid'
189 , secret = options.secret
190 , store = options.store || new MemoryStore
191 , fingerprint = options.fingerprint || defaultFingerprint
192 , cookie = options.cookie;
194 // notify user that this store is not
195 // meant for a production environment
196 if ('production' == env && store instanceof MemoryStore) {
197 console.warn(warning);
200 // ensure secret is present
202 throw new Error('connect.session({ secret: "string" }) required for security');
205 // session hashing function
206 store.hash = function(req, base) {
208 .createHmac('sha256', secret)
209 .update(base + fingerprint(req))
214 // generates the new session
215 store.generate = function(req){
216 var base = utils.uid(24);
217 var sessionID = base + '.' + store.hash(req, base);
218 req.sessionID = sessionID;
219 req.session = new Session(req);
220 req.session.cookie = new Cookie(cookie);
223 return function session(req, res, next) {
225 if (req.session) return next();
228 var url = parse(req.url)
229 , path = url.pathname;
232 if (~exports.ignore.indexOf(path)) return next();
235 req.sessionStore = store;
237 // proxy writeHead() to Set-Cookie
238 var writeHead = res.writeHead;
239 res.writeHead = function(status, headers){
241 var cookie = req.session.cookie;
242 // only send secure session cookies when there is a secure connection.
243 // proxySecure is a custom attribute to allow for a reverse proxy
244 // to handle SSL connections and to communicate to connect over HTTP that
245 // the incoming connection is secure.
246 var secured = cookie.secure && (req.connection.encrypted || req.connection.proxySecure);
247 if (secured || !cookie.secure) {
248 res.setHeader('Set-Cookie', cookie.serialize(key, req.sessionID));
252 res.writeHead = writeHead;
253 return res.writeHead(status, headers);
256 // proxy end() to commit the session
258 res.end = function(data, encoding){
261 // HACK: ensure Set-Cookie for implicit writeHead()
262 if (!res._header) res._implicitHeader();
263 req.session.resetMaxAge();
264 req.session.save(function(){
265 res.end(data, encoding);
268 res.end(data, encoding);
273 function hash(base) {
274 return store.hash(req, base);
277 // generate the session
278 function generate() {
282 // get the sessionID from the cookie
283 req.sessionID = req.cookies[key];
285 // make a new session if the browser doesn't send a sessionID
286 if (!req.sessionID) {
292 // check the fingerprint
293 var parts = req.sessionID.split('.');
294 if (parts[1] != hash(parts[0])) {
300 // generate the session object
301 var pause = utils.pause(req);
302 store.get(req.sessionID, function(err, sess){
303 // proxy to resume() events
305 next = function(err){
312 if ('ENOENT' == err.code) {
322 // populate req.session
324 store.createSession(req, sess);