kopia lustrzana https://github.com/c9/core
initial fork from connect/session
rodzic
23b45af7f1
commit
b9a5f68c2c
|
@ -0,0 +1,358 @@
|
||||||
|
/*!
|
||||||
|
* Connect - session
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Session = require('./session/session')
|
||||||
|
, debug = require('debug')('connect:session')
|
||||||
|
, MemoryStore = require('./session/memory')
|
||||||
|
, signature = require('cookie-signature')
|
||||||
|
, Cookie = require('./session/cookie')
|
||||||
|
, Store = require('./session/store')
|
||||||
|
, utils = require('./../utils')
|
||||||
|
, uid = require('uid2')
|
||||||
|
, crc32 = require('buffer-crc32')
|
||||||
|
, parse = require('url').parse;
|
||||||
|
|
||||||
|
// 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 will not scale past a single process.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session:
|
||||||
|
*
|
||||||
|
* 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()](cookieParser.html)
|
||||||
|
* middleware _before_ `session()`.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* connect()
|
||||||
|
* .use(connect.cookieParser())
|
||||||
|
* .use(connect.session({ secret: 'keyboard cat', key: 'sid', cookie: { secure: true }}))
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
*
|
||||||
|
* - `key` cookie name defaulting to `connect.sid`
|
||||||
|
* - `store` session store instance
|
||||||
|
* - `secret` session cookie is signed with this secret to prevent tampering
|
||||||
|
* - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }`
|
||||||
|
* - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto")
|
||||||
|
*
|
||||||
|
* Cookie option:
|
||||||
|
*
|
||||||
|
* By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set
|
||||||
|
* so the cookie becomes a browser-session cookie. When the user closes the
|
||||||
|
* browser the cookie (and session) will be removed.
|
||||||
|
*
|
||||||
|
* ## 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()
|
||||||
|
* .use(connect.favicon())
|
||||||
|
* .use(connect.cookieParser())
|
||||||
|
* .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
|
||||||
|
* .use(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` property. 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 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'
|
||||||
|
, store = options.store || new MemoryStore
|
||||||
|
, cookie = options.cookie || {}
|
||||||
|
, trustProxy = options.proxy
|
||||||
|
, storeReady = true
|
||||||
|
, rollingSessions = options.rolling || false;
|
||||||
|
|
||||||
|
// notify user that this store is not
|
||||||
|
// meant for a production environment
|
||||||
|
if ('production' == env && store instanceof MemoryStore) {
|
||||||
|
console.warn(warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generates the new session
|
||||||
|
store.generate = function(req){
|
||||||
|
req.sessionID = uid(24);
|
||||||
|
req.session = new Session(req);
|
||||||
|
req.session.cookie = new Cookie(cookie);
|
||||||
|
};
|
||||||
|
|
||||||
|
store.on('disconnect', function(){ storeReady = false; });
|
||||||
|
store.on('connect', function(){ storeReady = true; });
|
||||||
|
|
||||||
|
return function session(req, res, next) {
|
||||||
|
// self-awareness
|
||||||
|
if (req.session) return next();
|
||||||
|
|
||||||
|
// Handle connection as if there is no session if
|
||||||
|
// the store has temporarily disconnected etc
|
||||||
|
if (!storeReady) return debug('store is disconnected'), next();
|
||||||
|
|
||||||
|
// pathname mismatch
|
||||||
|
var originalPath = parse(req.originalUrl).pathname;
|
||||||
|
if (0 != originalPath.indexOf(cookie.path || '/')) return next();
|
||||||
|
|
||||||
|
// backwards compatibility for signed cookies
|
||||||
|
// req.secret is passed from the cookie parser middleware
|
||||||
|
var secret = options.secret || req.secret;
|
||||||
|
|
||||||
|
// ensure secret is available or bail
|
||||||
|
if (!secret) throw new Error('`secret` option required for sessions');
|
||||||
|
|
||||||
|
var originalHash
|
||||||
|
, originalId;
|
||||||
|
|
||||||
|
// expose store
|
||||||
|
req.sessionStore = store;
|
||||||
|
|
||||||
|
// grab the session cookie value and check the signature
|
||||||
|
var rawCookie = req.cookies[key];
|
||||||
|
|
||||||
|
// get signedCookies for backwards compat with signed cookies
|
||||||
|
var unsignedCookie = req.signedCookies[key];
|
||||||
|
|
||||||
|
if (!unsignedCookie && rawCookie) {
|
||||||
|
unsignedCookie = utils.parseSignedCookie(rawCookie, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set-cookie
|
||||||
|
res.on('header', function(){
|
||||||
|
if (!req.session) return;
|
||||||
|
var cookie = req.session.cookie
|
||||||
|
, proto = (req.headers['x-forwarded-proto'] || '').split(',')[0].toLowerCase().trim()
|
||||||
|
, tls = req.connection.encrypted || (trustProxy && 'https' == proto)
|
||||||
|
, isNew = unsignedCookie != req.sessionID;
|
||||||
|
|
||||||
|
// only send secure cookies via https
|
||||||
|
if (cookie.secure && !tls) return debug('not secured');
|
||||||
|
|
||||||
|
// in case of rolling session, always reset the cookie
|
||||||
|
if (!rollingSessions) {
|
||||||
|
|
||||||
|
// browser-session length cookie
|
||||||
|
if (null == cookie.expires) {
|
||||||
|
if (!isNew) return debug('already set browser-session cookie');
|
||||||
|
// compare hashes and ids
|
||||||
|
} else if (originalHash == hash(req.session) && originalId == req.session.id) {
|
||||||
|
return debug('unmodified session');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = 's:' + signature.sign(req.sessionID, secret);
|
||||||
|
val = cookie.serialize(key, val);
|
||||||
|
debug('set-cookie %s', val);
|
||||||
|
res.setHeader('Set-Cookie', val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// proxy end() to commit the session
|
||||||
|
var end = res.end;
|
||||||
|
res.end = function(data, encoding){
|
||||||
|
res.end = end;
|
||||||
|
if (!req.session) return res.end(data, encoding);
|
||||||
|
debug('saving');
|
||||||
|
req.session.resetMaxAge();
|
||||||
|
req.session.save(function(err){
|
||||||
|
if (err) console.error(err.stack);
|
||||||
|
debug('saved');
|
||||||
|
res.end(data, encoding);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// generate the session
|
||||||
|
function generate() {
|
||||||
|
store.generate(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the sessionID from the cookie
|
||||||
|
req.sessionID = unsignedCookie;
|
||||||
|
|
||||||
|
// generate a session if the browser doesn't send a sessionID
|
||||||
|
if (!req.sessionID) {
|
||||||
|
debug('no SID sent, generating session');
|
||||||
|
generate();
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the session object
|
||||||
|
var pause = utils.pause(req);
|
||||||
|
debug('fetching %s', req.sessionID);
|
||||||
|
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) {
|
||||||
|
debug('error %j', err);
|
||||||
|
if ('ENOENT' == err.code) {
|
||||||
|
generate();
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
// no session
|
||||||
|
} else if (!sess) {
|
||||||
|
debug('no session found');
|
||||||
|
generate();
|
||||||
|
next();
|
||||||
|
// populate req.session
|
||||||
|
} else {
|
||||||
|
debug('session found');
|
||||||
|
store.createSession(req, sess);
|
||||||
|
originalId = req.sessionID;
|
||||||
|
originalHash = hash(sess);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash the given `sess` object omitting changes
|
||||||
|
* to `.cookie`.
|
||||||
|
*
|
||||||
|
* @param {Object} sess
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function hash(sess) {
|
||||||
|
return crc32.signed(JSON.stringify(sess, function(key, val){
|
||||||
|
if ('cookie' != key) return val;
|
||||||
|
}));
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connect - session - Cookie
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var utils = require('../../utils')
|
||||||
|
, cookie = require('cookie');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new `Cookie` with the given `options`.
|
||||||
|
*
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @param {Object} options
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Cookie = module.exports = function Cookie(options) {
|
||||||
|
this.path = '/';
|
||||||
|
this.maxAge = null;
|
||||||
|
this.httpOnly = true;
|
||||||
|
if (options) utils.merge(this, options);
|
||||||
|
this.originalMaxAge = undefined == this.originalMaxAge
|
||||||
|
? this.maxAge
|
||||||
|
: this.originalMaxAge;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Prototype.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Cookie.prototype = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set expires `date`.
|
||||||
|
*
|
||||||
|
* @param {Date} date
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set expires(date) {
|
||||||
|
this._expires = date;
|
||||||
|
this.originalMaxAge = this.maxAge;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get expires `date`.
|
||||||
|
*
|
||||||
|
* @return {Date}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get expires() {
|
||||||
|
return this._expires;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set expires via max-age in `ms`.
|
||||||
|
*
|
||||||
|
* @param {Number} ms
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
set maxAge(ms) {
|
||||||
|
this.expires = 'number' == typeof ms
|
||||||
|
? new Date(Date.now() + ms)
|
||||||
|
: ms;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get expires max-age in `ms`.
|
||||||
|
*
|
||||||
|
* @return {Number}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
get maxAge() {
|
||||||
|
return this.expires instanceof Date
|
||||||
|
? this.expires.valueOf() - Date.now()
|
||||||
|
: this.expires;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return cookie data object.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return {
|
||||||
|
originalMaxAge: this.originalMaxAge
|
||||||
|
, expires: this._expires
|
||||||
|
, secure: this.secure
|
||||||
|
, httpOnly: this.httpOnly
|
||||||
|
, domain: this.domain
|
||||||
|
, path: this.path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a serialized cookie string.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
serialize: function(name, val){
|
||||||
|
return cookie.serialize(name, val, this.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return JSON representation of this cookie.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
toJSON: function(){
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,129 @@
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connect - session - MemoryStore
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Store = require('./store');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a new `MemoryStore`.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
var MemoryStore = module.exports = function MemoryStore() {
|
||||||
|
this.sessions = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherit from `Store.prototype`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.__proto__ = Store.prototype;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to fetch session by the given `sid`.
|
||||||
|
*
|
||||||
|
* @param {String} sid
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.get = function(sid, fn){
|
||||||
|
var self = this;
|
||||||
|
process.nextTick(function(){
|
||||||
|
var expires
|
||||||
|
, sess = self.sessions[sid];
|
||||||
|
if (sess) {
|
||||||
|
sess = JSON.parse(sess);
|
||||||
|
expires = 'string' == typeof sess.cookie.expires
|
||||||
|
? new Date(sess.cookie.expires)
|
||||||
|
: sess.cookie.expires;
|
||||||
|
if (!expires || new Date < expires) {
|
||||||
|
fn(null, sess);
|
||||||
|
} else {
|
||||||
|
self.destroy(sid, fn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit the given `sess` object associated with the given `sid`.
|
||||||
|
*
|
||||||
|
* @param {String} sid
|
||||||
|
* @param {Session} sess
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.set = function(sid, sess, fn){
|
||||||
|
var self = this;
|
||||||
|
process.nextTick(function(){
|
||||||
|
self.sessions[sid] = JSON.stringify(sess);
|
||||||
|
fn && fn();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the session associated with the given `sid`.
|
||||||
|
*
|
||||||
|
* @param {String} sid
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.destroy = function(sid, fn){
|
||||||
|
var self = this;
|
||||||
|
process.nextTick(function(){
|
||||||
|
delete self.sessions[sid];
|
||||||
|
fn && fn();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke the given callback `fn` with all active sessions.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.all = function(fn){
|
||||||
|
var arr = []
|
||||||
|
, keys = Object.keys(this.sessions);
|
||||||
|
for (var i = 0, len = keys.length; i < len; ++i) {
|
||||||
|
arr.push(this.sessions[keys[i]]);
|
||||||
|
}
|
||||||
|
fn(null, arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all sessions.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.clear = function(fn){
|
||||||
|
this.sessions = {};
|
||||||
|
fn && fn();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch number of sessions.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
MemoryStore.prototype.length = function(fn){
|
||||||
|
fn(null, Object.keys(this.sessions).length);
|
||||||
|
};
|
|
@ -0,0 +1,116 @@
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connect - session - Session
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var utils = require('../../utils');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new `Session` with the given request and `data`.
|
||||||
|
*
|
||||||
|
* @param {IncomingRequest} req
|
||||||
|
* @param {Object} data
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Session = module.exports = function Session(req, data) {
|
||||||
|
Object.defineProperty(this, 'req', { value: req });
|
||||||
|
Object.defineProperty(this, 'id', { value: req.sessionID });
|
||||||
|
if ('object' == typeof data) utils.merge(this, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update reset `.cookie.maxAge` to prevent
|
||||||
|
* the cookie from expiring when the
|
||||||
|
* session is still active.
|
||||||
|
*
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.touch = function(){
|
||||||
|
return this.resetMaxAge();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset `.maxAge` to `.originalMaxAge`.
|
||||||
|
*
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.resetMaxAge = function(){
|
||||||
|
this.cookie.maxAge = this.cookie.originalMaxAge;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the session data with optional callback `fn(err)`.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.save = function(fn){
|
||||||
|
this.req.sessionStore.set(this.id, this, fn || function(){});
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-loads the session data _without_ altering
|
||||||
|
* the maxAge properties. Invokes the callback `fn(err)`,
|
||||||
|
* after which time if no exception has occurred the
|
||||||
|
* `req.session` property will be a new `Session` object,
|
||||||
|
* although representing the same session.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.reload = function(fn){
|
||||||
|
var req = this.req
|
||||||
|
, store = this.req.sessionStore;
|
||||||
|
store.get(this.id, function(err, sess){
|
||||||
|
if (err) return fn(err);
|
||||||
|
if (!sess) return fn(new Error('failed to load session'));
|
||||||
|
store.createSession(req, sess);
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy `this` session.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.destroy = function(fn){
|
||||||
|
delete this.req.session;
|
||||||
|
this.req.sessionStore.destroy(this.id, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate this request's session.
|
||||||
|
*
|
||||||
|
* @param {Function} fn
|
||||||
|
* @return {Session} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Session.prototype.regenerate = function(fn){
|
||||||
|
this.req.sessionStore.regenerate(this.req, fn);
|
||||||
|
return this;
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connect - session - Store
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var EventEmitter = require('events').EventEmitter
|
||||||
|
, Session = require('./session')
|
||||||
|
, Cookie = require('./cookie');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize abstract `Store`.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Store = module.exports = function Store(options){};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherit from `EventEmitter.prototype`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Store.prototype.__proto__ = EventEmitter.prototype;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate the given requests's session.
|
||||||
|
*
|
||||||
|
* @param {IncomingRequest} req
|
||||||
|
* @return {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Store.prototype.regenerate = function(req, fn){
|
||||||
|
var self = this;
|
||||||
|
this.destroy(req.sessionID, function(err){
|
||||||
|
self.generate(req);
|
||||||
|
fn(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a `Session` instance via the given `sid`
|
||||||
|
* and invoke the callback `fn(err, sess)`.
|
||||||
|
*
|
||||||
|
* @param {String} sid
|
||||||
|
* @param {Function} fn
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Store.prototype.load = function(sid, fn){
|
||||||
|
var self = this;
|
||||||
|
this.get(sid, function(err, sess){
|
||||||
|
if (err) return fn(err);
|
||||||
|
if (!sess) return fn();
|
||||||
|
var req = { sessionID: sid, sessionStore: self };
|
||||||
|
sess = self.createSession(req, sess);
|
||||||
|
fn(null, sess);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create session from JSON `sess` data.
|
||||||
|
*
|
||||||
|
* @param {IncomingRequest} req
|
||||||
|
* @param {Object} sess
|
||||||
|
* @return {Session}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
Store.prototype.createSession = function(req, sess){
|
||||||
|
var expires = sess.cookie.expires
|
||||||
|
, orig = sess.cookie.originalMaxAge;
|
||||||
|
sess.cookie = new Cookie(sess.cookie);
|
||||||
|
if ('string' == typeof expires) sess.cookie.expires = new Date(expires);
|
||||||
|
sess.cookie.originalMaxAge = orig;
|
||||||
|
req.session = new Session(req, sess);
|
||||||
|
return req.session;
|
||||||
|
};
|
|
@ -0,0 +1,408 @@
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connect - utils
|
||||||
|
* Copyright(c) 2010 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var http = require('http')
|
||||||
|
, crypto = require('crypto')
|
||||||
|
, parse = require('url').parse
|
||||||
|
, sep = require('path').sep
|
||||||
|
, signature = require('cookie-signature')
|
||||||
|
, nodeVersion = process.versions.node.split('.');
|
||||||
|
|
||||||
|
// pause is broken in node < 0.10
|
||||||
|
exports.brokenPause = parseInt(nodeVersion[0], 10) === 0
|
||||||
|
&& parseInt(nodeVersion[1], 10) < 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if the request has a body, otherwise return `false`.
|
||||||
|
*
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.hasBody = function(req) {
|
||||||
|
var encoding = 'transfer-encoding' in req.headers;
|
||||||
|
var length = 'content-length' in req.headers && req.headers['content-length'] !== '0';
|
||||||
|
return encoding || length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the mime type from the given request's
|
||||||
|
* _Content-Type_ header.
|
||||||
|
*
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.mime = function(req) {
|
||||||
|
var str = req.headers['content-type'] || '';
|
||||||
|
return str.split(';')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an `Error` from the given status `code`
|
||||||
|
* and optional `msg`.
|
||||||
|
*
|
||||||
|
* @param {Number} code
|
||||||
|
* @param {String} msg
|
||||||
|
* @return {Error}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.error = function(code, msg){
|
||||||
|
var err = new Error(msg || http.STATUS_CODES[code]);
|
||||||
|
err.status = code;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return md5 hash of the given string and optional encoding,
|
||||||
|
* defaulting to hex.
|
||||||
|
*
|
||||||
|
* utils.md5('wahoo');
|
||||||
|
* // => "e493298061761236c96b02ea6aa8a2ad"
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @param {String} encoding
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.md5 = function(str, encoding){
|
||||||
|
return crypto
|
||||||
|
.createHash('md5')
|
||||||
|
.update(str, 'utf8')
|
||||||
|
.digest(encoding || 'hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge object b with object a.
|
||||||
|
*
|
||||||
|
* var a = { foo: 'bar' }
|
||||||
|
* , b = { bar: 'baz' };
|
||||||
|
*
|
||||||
|
* utils.merge(a, b);
|
||||||
|
* // => { foo: 'bar', bar: 'baz' }
|
||||||
|
*
|
||||||
|
* @param {Object} a
|
||||||
|
* @param {Object} b
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.merge = function(a, b){
|
||||||
|
if (a && b) {
|
||||||
|
for (var key in b) {
|
||||||
|
a[key] = b[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape the given string of `html`.
|
||||||
|
*
|
||||||
|
* @param {String} html
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.escape = function(html){
|
||||||
|
return String(html)
|
||||||
|
.replace(/&(?!\w+;)/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign the given `val` with `secret`.
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
* @param {String} secret
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.sign = function(val, secret){
|
||||||
|
console.warn('do not use utils.sign(), use https://github.com/visionmedia/node-cookie-signature')
|
||||||
|
return val + '.' + crypto
|
||||||
|
.createHmac('sha256', secret)
|
||||||
|
.update(val)
|
||||||
|
.digest('base64')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsign and decode the given `val` with `secret`,
|
||||||
|
* returning `false` if the signature is invalid.
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
* @param {String} secret
|
||||||
|
* @return {String|Boolean}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.unsign = function(val, secret){
|
||||||
|
console.warn('do not use utils.unsign(), use https://github.com/visionmedia/node-cookie-signature')
|
||||||
|
var str = val.slice(0, val.lastIndexOf('.'));
|
||||||
|
return exports.sign(str, secret) == val
|
||||||
|
? str
|
||||||
|
: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse signed cookies, returning an object
|
||||||
|
* containing the decoded key/value pairs,
|
||||||
|
* while removing the signed key from `obj`.
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseSignedCookies = function(obj, secret){
|
||||||
|
var ret = {};
|
||||||
|
Object.keys(obj).forEach(function(key){
|
||||||
|
var val = obj[key];
|
||||||
|
if (0 == val.indexOf('s:')) {
|
||||||
|
val = signature.unsign(val.slice(2), secret);
|
||||||
|
if (val) {
|
||||||
|
ret[key] = val;
|
||||||
|
delete obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a signed cookie string, return the decoded value
|
||||||
|
*
|
||||||
|
* @param {String} str signed cookie string
|
||||||
|
* @param {String} secret
|
||||||
|
* @return {String} decoded value
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseSignedCookie = function(str, secret){
|
||||||
|
return 0 == str.indexOf('s:')
|
||||||
|
? signature.unsign(str.slice(2), secret)
|
||||||
|
: str;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON cookies.
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseJSONCookies = function(obj){
|
||||||
|
Object.keys(obj).forEach(function(key){
|
||||||
|
var val = obj[key];
|
||||||
|
var res = exports.parseJSONCookie(val);
|
||||||
|
if (res) obj[key] = res;
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse JSON cookie string
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @return {Object} Parsed object or null if not json cookie
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseJSONCookie = function(str) {
|
||||||
|
if (0 == str.indexOf('j:')) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str.slice(2));
|
||||||
|
} catch (err) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause `data` and `end` events on the given `obj`.
|
||||||
|
* Middleware performing async tasks _should_ utilize
|
||||||
|
* this utility (or similar), to re-emit data once
|
||||||
|
* the async operation has completed, otherwise these
|
||||||
|
* events may be lost. Pause is only required for
|
||||||
|
* node versions less than 10, and is replaced with
|
||||||
|
* noop's otherwise.
|
||||||
|
*
|
||||||
|
* var pause = utils.pause(req);
|
||||||
|
* fs.readFile(path, function(){
|
||||||
|
* next();
|
||||||
|
* pause.resume();
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.pause = exports.brokenPause
|
||||||
|
? require('pause')
|
||||||
|
: function () {
|
||||||
|
return {
|
||||||
|
end: noop,
|
||||||
|
resume: noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip `Content-*` headers from `res`.
|
||||||
|
*
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.removeContentHeaders = function(res){
|
||||||
|
if (!res._headers) return;
|
||||||
|
Object.keys(res._headers).forEach(function(field){
|
||||||
|
if (0 == field.indexOf('content')) {
|
||||||
|
res.removeHeader(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `req` is a conditional GET request.
|
||||||
|
*
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.conditionalGET = function(req) {
|
||||||
|
return req.headers['if-modified-since']
|
||||||
|
|| req.headers['if-none-match'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond with 401 "Unauthorized".
|
||||||
|
*
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {String} realm
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.unauthorized = function(res, realm) {
|
||||||
|
res.statusCode = 401;
|
||||||
|
res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"');
|
||||||
|
res.end('Unauthorized');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond with 304 "Not Modified".
|
||||||
|
*
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {Object} headers
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.notModified = function(res) {
|
||||||
|
exports.removeContentHeaders(res);
|
||||||
|
res.statusCode = 304;
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an ETag in the form of `"<size>-<mtime>"`
|
||||||
|
* from the given `stat`.
|
||||||
|
*
|
||||||
|
* @param {Object} stat
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.etag = function(stat) {
|
||||||
|
return '"' + stat.size + '-' + Number(stat.mtime) + '"';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the given Cache-Control `str`.
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseCacheControl = function(str){
|
||||||
|
var directives = str.split(',')
|
||||||
|
, obj = {};
|
||||||
|
|
||||||
|
for(var i = 0, len = directives.length; i < len; i++) {
|
||||||
|
var parts = directives[i].split('=')
|
||||||
|
, key = parts.shift().trim()
|
||||||
|
, val = parseInt(parts.shift(), 10);
|
||||||
|
|
||||||
|
obj[key] = isNaN(val) ? true : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the `req` url with memoization.
|
||||||
|
*
|
||||||
|
* @param {ServerRequest} req
|
||||||
|
* @return {Object}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseUrl = function(req){
|
||||||
|
var parsed = req._parsedUrl;
|
||||||
|
if (parsed && parsed.href == req.url) {
|
||||||
|
return parsed;
|
||||||
|
} else {
|
||||||
|
parsed = parse(req.url);
|
||||||
|
|
||||||
|
if (parsed.auth && !parsed.protocol && ~parsed.href.indexOf('//')) {
|
||||||
|
// This parses pathnames, and a strange pathname like //r@e should work
|
||||||
|
parsed = parse(req.url.replace(/@/g, '%40'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return req._parsedUrl = parsed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse byte `size` string.
|
||||||
|
*
|
||||||
|
* @param {String} size
|
||||||
|
* @return {Number}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parseBytes = require('bytes');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the path separator from system separator
|
||||||
|
* to URL separator, aka `/`.
|
||||||
|
*
|
||||||
|
* @param {String} path
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.normalizeSlashes = function normalizeSlashes(path) {
|
||||||
|
return path.split(sep).join('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
function noop() {}
|
Ładowanie…
Reference in New Issue