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