restructured tests to reduce crazy concurrency, also no sharing of express apps between tobis, tobis tend to be jealous, and added check that only single cookie is set, fixed bug where reset() was setting cookie

This commit is contained in:
Ben Adida 2011-12-30 12:25:01 -08:00
Родитель ed7e6b0f8b
Коммит 40b0cb2306
2 изменённых файлов: 204 добавлений и 117 удалений

Просмотреть файл

@ -37,14 +37,15 @@ function Session(req, res, cookies, opts) {
this.res = res;
this.cookies = cookies;
this.opts = opts;
this.content = {};
this.loaded = false;
this.dirty = false;
// support for maxAge
if (opts.cookie.maxAge) {
this.expires = new Date(new Date().getTime() + opts.cookie.maxAge);
}
this.content = {};
this.loaded = false;
this.dirty = false;
this.createdAt = new Date().getTime();
}
Session.prototype = {
@ -61,14 +62,15 @@ Session.prototype = {
reset: function(keysToPreserve) {
this.clearContent(keysToPreserve);
this.createdAt = new Date().getTime();
this.dirty = true;
this.updateCookie();
},
// take the content and do the encrypt-and-sign
// boxing builds in the concept of createdAt
box: function() {
// format will be:
// iv.ciphertext.hmac
// iv.ciphertext.createdAt.hmac
// generate iv
var iv = crypto.randomBytes(16).toString('binary');
@ -81,10 +83,13 @@ Session.prototype = {
// hmac it
var hmacAlg = crypto.createHmac('sha256', this.opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(this.createdAt.toString());
var hmac = hmacAlg.digest();
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." +base64urlencode(hmac);
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + this.createdAt + "." + base64urlencode(hmac);
},
unbox: function(content) {
@ -93,24 +98,28 @@ Session.prototype = {
// stop at any time if there's an issue
var components = content.split(".");
if (components.length != 3)
return;
if (components.length != 4)
return false;
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var hmac = base64urldecode(components[2]);
var createdAt = parseInt(components[2]);
var hmac = base64urldecode(components[3]);
// make sure IV is right length
if (iv.length != 16)
return;
return false;
// check hmac
var hmacAlg = crypto.createHmac('sha256', this.opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
var expected_hmac = hmacAlg.digest();
if (hmac != expected_hmac)
return;
return false;
// decrypt
var cipher = crypto.createDecipheriv('aes256', this.opts.encryptionKey, iv);
@ -124,8 +133,12 @@ Session.prototype = {
self.content[k] = new_content[k];
});
} catch (x) {
// do nothing
return false;
}
// all is well, accept creation time
this.createdAt = createdAt;
return true;
},
updateCookie: function() {

Просмотреть файл

@ -6,14 +6,21 @@ var vows = require("vows"),
tobi = require("tobi"),
Browser = require("zombie");
// set up the session middleware
var middleware = cookieSessions({
cookieName: 'session',
secret: 'yo',
cookie: {
maxAge: 5000
}
});
function create_app() {
// set up the session middleware
var middleware = cookieSessions({
cookieName: 'session',
secret: 'yo',
cookie: {
maxAge: 5000
}
});
var app = express.createServer();
app.use(middleware);
return app;
}
var suite = vows.describe('all');
@ -22,14 +29,12 @@ suite.addBatch({
topic: function() {
var self = this;
// simple app
var app = express.createServer();
app.use(middleware);
var app = create_app();
app.get("/foo", function(req, res) {
self.callback(null, req);
res.send("hello");
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {});
},
@ -73,8 +78,8 @@ suite.addBatch({
var self = this;
// simple app
var app = express.createServer();
app.use(middleware);
var app = create_app();
app.get("/foo", function(req, res) {
req.session.foo = 'foobar';
res.send("hello");
@ -88,6 +93,9 @@ suite.addBatch({
"includes a set-cookie header": function(err, res) {
assert.isArray(res.headers['set-cookie']);
},
"only one set-cookie header": function(err, res) {
assert.equal(res.headers['set-cookie'].length, 1);
},
"with an expires attribute": function(err, res) {
assert.match(res.headers['set-cookie'][0], /expires/);
},
@ -106,8 +114,8 @@ suite.addBatch({
var self = this;
// simple app
var app = express.createServer();
app.use(middleware);
var app = create_app();
app.get("/foo", function(req, res) {
req.session.reset();
req.session.foo = 'foobar';
@ -142,8 +150,8 @@ suite.addBatch({
var self = this;
// simple app
var app = express.createServer();
app.use(middleware);
var app = create_app();
app.get("/foo", function(req, res) {
req.session.reset();
req.session.foo = 'foobar';
@ -182,8 +190,8 @@ suite.addBatch({
var self = this;
// simple app
var app = express.createServer();
app.use(middleware);
var app = create_app();
app.get("/foo", function(req, res) {
req.session.foo = 'foobar';
res.send("foo");
@ -208,103 +216,169 @@ suite.addBatch({
});
suite.addBatch({
"short duration" : {
"writing to a session" : {
topic: function() {
var self = this;
// simple app
var app = express.createServer();
app.use(cookieSessions({
cookieName: 'session',
secret: 'yo',
duration: 500 // 0.5 seconds
}));
var app = create_app();
app.get("/foo", function(req, res) {
req.session.reset();
req.session.foo = 'foobar';
res.send("foo");
});
return app;
},
"querying within the duration time": {
topic: function(app) {
var self = this;
app.get("/bar", function(req, res) {
self.callback(null, req);
res.send("bar");
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
setTimeout(function () {
browser.get("/bar", function(res, $) {
});
}, 200);
});
},
"session still has state": function(err, req) {
assert.equal(req.session.foo, 'foobar');
}
},
"querying outside the duration time": {
topic: function(app) {
var self = this;
app.get("/bar", function(req, res) {
req.session.reset();
req.session.reset();
req.session.bar = 'bar';
req.session.baz = 'baz';
res.send("bar");
});
app.get("/bar2", function(req, res) {
self.callback(null, req);
res.send("bar2");
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
browser.get("/bar", function(res, $) {
// observe the response to the second request
self.callback(null, res);
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
setTimeout(function () {
browser.get("/bar2", function(res, $) {
});
}, 800);
});
},
"session no longer has state": function(err, req) {
assert.isUndefined(req.session.foo);
}
});
},
"querying twice, each at 3/4 duration time": {
topic: function(app) {
var self = this;
app.get("/bar3", function(req, res) {
req.session.baz = Math.random();
res.send("bar3");
});
app.get("/bar4", function(req, res) {
self.callback(null, req);
res.send("bar4");
});
var browser = tobi.createBrowser(app);
// first query resets the session to full duration
browser.get("/foo", function(res, $) {
setTimeout(function () {
// this query should NOT reset the session
browser.get("/bar3", function(res, $) {
setTimeout(function () {
// so the session should be dead by now
browser.get("/bar4", function(res, $) {
});
}, 300);
});
}, 300);
});
},
"session no longer has state": function(err, req) {
assert.isUndefined(req.session.baz);
}
"sets a cookie": function(err, res) {
assert.isArray(res.headers['set-cookie']);
},
"and only one cookie": function(err, res) {
assert.equal(res.headers['set-cookie'].length, 1);
}
}
});
function create_app_with_duration() {
// simple app
var app = express.createServer();
app.use(cookieSessions({
cookieName: 'session',
secret: 'yo',
duration: 500 // 0.5 seconds
}));
app.get("/foo", function(req, res) {
req.session.reset();
req.session.foo = 'foobar';
res.send("foo");
});
return app;
}
suite.addBatch({
"querying within duration" : {
topic: function() {
var self = this;
var app = create_app_with_duration();
app.get("/bar", function(req, res) {
self.callback(null, req);
res.send("bar");
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
setTimeout(function () {
browser.get("/bar", function(res, $) {
});
}, 200);
});
},
"session still has state": function(err, req) {
assert.equal(req.session.foo, 'foobar');
}
}
});
suite.addBatch({
"modifying the session": {
topic: function() {
var self = this;
var app = create_app_with_duration();
app.get("/bar", function(req, res) {
self.callback(null, req);
res.send("bar");
});
var browser = tobi.createBrowser(app);
var firstCreatedAt, secondCreatedAt;
browser.get("/foo", function(res, $) {
browser.get("/bar", function(res, $) {
});
});
},
"doesn't change createdAt": function(err, req) {
assert.equal(req.session.foo, 'foobar');
}
}
});
suite.addBatch({
"querying outside the duration time": {
topic: function() {
var self = this;
var app = create_app_with_duration();
app.get("/bar", function(req, res) {
self.callback(null, req);
res.send("bar");
});
var browser = tobi.createBrowser(app);
browser.get("/foo", function(res, $) {
setTimeout(function () {
browser.get("/bar", function(res, $) {
});
}, 800);
});
},
"session no longer has state": function(err, req) {
assert.isUndefined(req.session.foo);
}
}
});
suite.addBatch({
"querying twice, each at 3/4 duration time": {
topic: function() {
var self = this;
var app = create_app_with_duration();
app.get("/bar", function(req, res) {
req.session.baz = Math.random();
res.send("bar");
});
app.get("/bar2", function(req, res) {
self.callback(null, req);
res.send("bar2");
});
var browser = tobi.createBrowser(app);
// first query resets the session to full duration
browser.get("/foo", function(res, $) {
setTimeout(function () {
// this query should NOT reset the session
browser.get("/bar", function(res, $) {
setTimeout(function () {
// so the session should be dead by now
browser.get("/bar2", function(res, $) {
});
}, 300);
});
}, 300);
});
},
"session no longer has state": function(err, req) {
assert.isUndefined(req.session.baz);
}
}
});