345 lines · 9.2 KB · JavaScript
RawDownload
 /*! * 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) {  return '';}; /** * 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();      }    });  };};