Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Session Tuning #14

Open
wants to merge 9 commits into from

1 participant

@publickeating

Switched branches. Here is the original message:

I’ve been using cookie-sessions for a while and thought I’d push back the additions I made. I tried to make it into nice clean commits, but had to make a few corrections along the way. The key changes are that you can now set the 'domain' and make the cookie 'secure' as well as that 'max-age' will be sent if useMaxAge is true (default). Just to be complete, I also made it so that you can also selectively turn off setting 'expires' and 'HttpOnly' with useExpires:false and useHttpOnly: false, but the defaults are that they’re true so it shouldn’t change any existing uses of cookie-sessions.

publickeating added some commits
@publickeating publickeating Code cleanup to keep jslint from complaining. No functional changes. 9b47888
@publickeating publickeating I added a few options for tuning the cookies. You can now set the 'do…
…main' and make the cookie 'secure' as well as 'max-age' will be sent if useMaxAge is true. Just to be complete, I made it so that you could also selectively turn on/off setting 'expires' and 'HttpOnly' with useExpires and useHttpOnly options, but the defaults shouldn't change any existing uses of cookie-sessions, except that useMaxAge is true by default.
292749b
@publickeating publickeating Don't set cookieStr parameters if it doesn't already exist cd8bd61
@publickeating publickeating nodeunit says .testrunner is deprecated, use .reporters.default instead 41390b8
@publickeating publickeating Added unit tests for the new functionality f7159f6
@publickeating publickeating Let the expires() function change timeout into milliseconds, which ma…
…kes it work with existing unit tests and probably a little more semantically correct.
43f9509
@publickeating publickeating Adjust timestamp (which is in seconds) into milliseconds for comparis…
…on with Date.
f278270
@publickeating publickeating Don't mess with success! Switched back to expecting timeout to be in …
…milliseconds, not in seconds.
02f081f
@publickeating publickeating Whitespace 8c11acc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 13, 2011
  1. @publickeating
  2. @publickeating

    I added a few options for tuning the cookies. You can now set the 'do…

    publickeating authored
    …main' and make the cookie 'secure' as well as 'max-age' will be sent if useMaxAge is true. Just to be complete, I made it so that you could also selectively turn on/off setting 'expires' and 'HttpOnly' with useExpires and useHttpOnly options, but the defaults shouldn't change any existing uses of cookie-sessions, except that useMaxAge is true by default.
  3. @publickeating
  4. @publickeating
  5. @publickeating
  6. @publickeating

    Let the expires() function change timeout into milliseconds, which ma…

    publickeating authored
    …kes it work with existing unit tests and probably a little more semantically correct.
Commits on May 15, 2011
  1. @publickeating
Commits on May 19, 2011
  1. @publickeating

    Don't mess with success! Switched back to expecting timeout to be in …

    publickeating authored
    …milliseconds, not in seconds.
Commits on May 30, 2011
  1. @publickeating

    Whitespace

    publickeating authored
This page is out of date. Refresh to see the latest.
Showing with 209 additions and 65 deletions.
  1. +62 −49 lib/cookie-sessions.js
  2. +1 −1  test.js
  3. +146 −15 test/test-cookie-sessions.js
View
111 lib/cookie-sessions.js
@@ -1,23 +1,42 @@
+/*globals escape unescape */
+
var crypto = require('crypto');
var url = require('url');
-var exports = module.exports = function(settings){
+
+// Extend a given object with all the properties in passed-in object(s).
+// From underscore.js (http://documentcloud.github.com/underscore/)
+function extend(obj) {
+ Array.prototype.slice.call(arguments).forEach(function(source) {
+ for (var prop in source) obj[prop] = source[prop];
+ });
+ return obj;
+}
+
+var exports;
+exports = module.exports = function(settings){
var default_settings = {
// don't set a default cookie secret, must be explicitly defined
session_key: '_node',
- timeout: 1000 * 60 * 60 * 24, // 24 hours
- path: '/'
+ timeout: 60 * 60 * 24 * 1000, // 24 hours in milliseconds
+ path: '/',
+ domain: null,
+ secure: false,
+ useMaxAge: true,
+ useExpires: true,
+ useHttpOnly: true
};
var s = extend(default_settings, settings);
if(!s.secret) throw new Error('No secret set in cookie-session settings');
- if(typeof s.path !== 'string' || s.path.indexOf('/') != 0)
+ if(typeof s.path !== 'string' || s.path.indexOf('/') !== 0) {
throw new Error('invalid cookie path, must start with "/"');
+ }
return function(req, res, next){
// if the request is not under the specified path, do nothing.
- if (url.parse(req.url).pathname.indexOf(s.path) != 0) {
+ if (url.parse(req.url).pathname.indexOf(s.path) !== 0) {
next();
return;
}
@@ -47,29 +66,32 @@ var exports = module.exports = function(settings){
var cookiestr;
if (req.session === undefined) {
if ("cookie" in req.headers) {
- cookiestr = escape(s.session_key) + '='
- + '; expires=' + exports.expires(0)
- + '; path=' + s.path + '; HttpOnly';
+ cookiestr = escape(s.session_key) + '=';
+ s.timeout = 0;
}
} else {
- cookiestr = escape(s.session_key) + '='
- + escape(exports.serialize(s.secret, req.session))
- + '; expires=' + exports.expires(s.timeout)
- + '; path=' + s.path + '; HttpOnly';
+ cookiestr = escape(s.session_key) + '=' + escape(exports.serialize(s.secret, req.session));
}
-
+
if (cookiestr !== undefined) {
- if(Array.isArray(headers)) headers.push(['Set-Cookie', cookiestr]);
- else {
+ if (s.useExpires) cookiestr += '; expires=' + exports.expires(s.timeout);
+ if (s.useMaxAge) cookiestr += '; max-age=' + (s.timeout / 1000); // In seconds
+ if (s.path) cookiestr += '; path=' + s.path;
+ if (s.domain) cookiestr += '; domain=' + s.domain;
+ if (s.secure) cookiestr += '; secure';
+ if (s.useHttpOnly) cookiestr += '; HttpOnly';
+
+ if(Array.isArray(headers)) {
+ headers.push(['Set-Cookie', cookiestr]);
+ } else {
// if a Set-Cookie header already exists, convert headers to
// array so we can send multiple Set-Cookie headers.
- if(headers['Set-Cookie'] !== undefined){
+ if (headers['Set-Cookie'] !== undefined) {
headers = exports.headersToArray(headers);
headers.push(['Set-Cookie', cookiestr]);
- }
- // if no Set-Cookie header exists, leave the headers as an
- // object, and add a Set-Cookie property
- else {
+ } else {
+ // if no Set-Cookie header exists, leave the headers as an
+ // object, and add a Set-Cookie property
headers['Set-Cookie'] = cookiestr;
}
}
@@ -81,7 +103,7 @@ var exports = module.exports = function(settings){
}
// call the original writeHead on the request
return _writeHead.apply(res, args);
- }
+ };
next();
};
@@ -95,21 +117,11 @@ exports.headersToArray = function(headers){
}, []);
};
-
-// Extend a given object with all the properties in passed-in object(s).
-// From underscore.js (http://documentcloud.github.com/underscore/)
-function extend(obj) {
- Array.prototype.slice.call(arguments).forEach(function(source) {
- for (var prop in source) obj[prop] = source[prop];
- });
- return obj;
-};
-
exports.deserialize = function(secret, timeout, str){
// Parses a secure cookie string, returning the object stored within it.
// Throws an exception if the secure cookie string does not validate.
- if(!exports.valid(secret, timeout, str)){
+ if(!exports.valid(secret, timeout, str)) {
throw new Error('invalid cookie');
}
var data = exports.decrypt(secret, exports.split(str).data_blob);
@@ -121,7 +133,7 @@ exports.serialize = function(secret, data){
var data_str = JSON.stringify(data);
var data_enc = exports.encrypt(secret, data_str);
- var timestamp = (new Date()).getTime();
+ var timestamp = new Date().getTime();
var hmac_sig = exports.hmac_signature(secret, timestamp, data_enc);
var result = hmac_sig + timestamp + data_enc;
if(!exports.checkLength(result)){
@@ -156,6 +168,7 @@ exports.valid = function(secret, timeout, str){
var hmac_sig = exports.hmac_signature(
secret, parts.timestamp, parts.data_blob
);
+
return (
parts.hmac_signature === hmac_sig &&
parts.timestamp + timeout > new Date().getTime()
@@ -184,21 +197,20 @@ exports.readCookies = function(req){
// will already contain the parsed cookies
if (req.cookies) {
return req.cookies;
+ } else {
+ // Extracts the cookies from a request object.
+ var cookie = req.headers.cookie;
+ if(!cookie){
+ return {};
}
- else {
- // Extracts the cookies from a request object.
- var cookie = req.headers.cookie;
- if(!cookie){
- return {};
- }
- var parts = cookie.split(/\s*;\s*/g).map(function(x){
- return x.split('=');
- });
- return parts.reduce(function(a, x){
- a[unescape(x[0])] = unescape(x[1]);
- return a;
- }, {});
- }
+ var parts = cookie.split(/\s*;\s*/g).map(function(x){
+ return x.split('=');
+ });
+ return parts.reduce(function(a, x){
+ a[unescape(x[0])] = unescape(x[1]);
+ return a;
+ }, {});
+ }
};
exports.readSession = function(key, secret, timeout, req){
@@ -212,7 +224,8 @@ exports.readSession = function(key, secret, timeout, req){
return undefined;
};
-
+// Generates an expires date
+// @params timeout the time in milliseconds before the cookie expires
exports.expires = function(timeout){
- return (new Date(new Date().getTime() + (timeout))).toUTCString();
+ return new Date(new Date().getTime() + timeout).toUTCString();
};
View
2  test.js
@@ -5,7 +5,7 @@ require.paths.push(__dirname + '/deps');
require.paths.push(__dirname + '/lib');
try {
- var testrunner = require('nodeunit').testrunner;
+ var testrunner = require('nodeunit').reporters.default;
}
catch(e) {
var sys = require('sys');
View
161 test/test-cookie-sessions.js
@@ -319,14 +319,14 @@ exports['onRequest'] = function(test){
var s = {
session_key:'_node',
secret: 'secret',
- timeout: 86400
+ timeout: 86400000
};
var req = {url: '/'};
sessions.readSession = function(key, secret, timeout, req){
test.equals(key, '_node', 'readSession called with session key');
test.equals(secret, 'secret', 'readSession called with secret');
- test.equals(timeout, 86400, 'readSession called with timeout');
+ test.equals(timeout, 86400000, 'readSession called with timeout');
return 'testsession';
};
var next = function(){
@@ -348,7 +348,7 @@ exports['writeHead'] = function(test){
var s = {
session_key:'_node',
secret: 'secret',
- timeout: 86400
+ timeout: 86400000
};
var req = {headers: {cookie: "_node="}, url: '/'};
var res = {
@@ -357,6 +357,7 @@ exports['writeHead'] = function(test){
headers['Set-Cookie'],
'_node=serialized_session; ' +
'expires=expiry_date; ' +
+ 'max-age=86400; ' +
'path=/; HttpOnly'
);
test.equals(headers['original'], 'header');
@@ -394,7 +395,7 @@ exports['writeHead doesnt write cookie if none exists and session is undefined']
var s = {
session_key:'_node',
secret: 'secret',
- timeout: 86400
+ timeout: 86400000
};
var req = {headers: {}, url: '/'};
var res = {
@@ -419,7 +420,7 @@ exports['writeHead writes empty cookie with immediate expiration if session is u
var s = {
session_key:'_node',
secret: 'secret',
- timeout: 86400
+ timeout: 86400000
};
var req = {headers: {cookie: "_node=Blah"}, url: '/'};
var res = {
@@ -428,6 +429,7 @@ exports['writeHead writes empty cookie with immediate expiration if session is u
headers['Set-Cookie'],
'_node=; ' +
'expires=now; ' +
+ 'max-age=0; ' +
'path=/; HttpOnly'
);
test.equals(headers['original'], 'header');
@@ -488,7 +490,7 @@ exports['set multiple cookies'] = function(test){
var _expires = sessions.expires;
sessions.expires = function(timeout){
- test.equals(timeout, 12345);
+ test.equals(timeout, 12345000);
return 'expiry_date';
};
@@ -500,6 +502,7 @@ exports['set multiple cookies'] = function(test){
['Set-Cookie', 'testcookie=testvalue'],
['Set-Cookie', '_node=session_data; ' +
'expires=expiry_date; ' +
+ 'max-age=12345; ' +
'path=/; HttpOnly']
]);
sessions.serialize = _serialize;
@@ -507,7 +510,7 @@ exports['set multiple cookies'] = function(test){
test.done();
}};
- sessions({secret: 'secret', timeout: 12345})(req, res, function(){
+ sessions({secret: 'secret', timeout: 12345000})(req, res, function(){
req.session = {test: 'test'};
res.writeHead(200, {
'other_header': 'val',
@@ -525,7 +528,7 @@ exports['set single cookie'] = function(test){
var _expires = sessions.expires;
sessions.expires = function(timeout){
- test.equals(timeout, 12345);
+ test.equals(timeout, 12345000);
return 'expiry_date';
};
@@ -536,13 +539,14 @@ exports['set single cookie'] = function(test){
'other_header': 'val',
'Set-Cookie': '_node=session_data; ' +
'expires=expiry_date; ' +
+ 'max-age=12345; ' +
'path=/; HttpOnly'
});
sessions.serialize = _serialize;
sessions.expires = _expires;
test.done();
}};
- sessions({secret: 'secret', timeout: 12345})(req, res, function(){
+ sessions({secret: 'secret', timeout: 12345000})(req, res, function(){
req.session = {test: 'test'};
res.writeHead(200, {'other_header': 'val'});
});
@@ -557,7 +561,7 @@ exports['handle headers as array'] = function(test){
var _expires = sessions.expires;
sessions.expires = function(timeout){
- test.equals(timeout, 12345);
+ test.equals(timeout, 12345000);
return 'expiry_date';
};
@@ -569,12 +573,13 @@ exports['handle headers as array'] = function(test){
['header2', 'val2'],
['Set-Cookie', '_node=session_data; ' +
'expires=expiry_date; ' +
+ 'max-age=12345; ' +
'path=/; HttpOnly']
]);
sessions.serialize = _serialize;
test.done();
}};
- sessions({secret: 'secret', timeout: 12345})(req, res, function(){
+ sessions({secret: 'secret', timeout: 12345000})(req, res, function(){
req.session = {test: 'test'};
res.writeHead(200, [['header1', 'val1'],['header2', 'val2']]);
});
@@ -602,7 +607,7 @@ exports['send cookies even if there are no headers'] = function (test) {
test.done();
}
};
- sessions({secret: 'secret', timeout: 12345})(req, res, function () {
+ sessions({secret: 'secret', timeout: 12345000})(req, res, function () {
req.session = {test: 'test'};
res.writeHead(200);
});
@@ -619,7 +624,7 @@ exports['send cookies when no headers but reason_phrase'] = function (test) {
test.done();
}
};
- sessions({secret: 'secret', timeout: 12345})(req, res, function () {
+ sessions({secret: 'secret', timeout: 12345000})(req, res, function () {
req.session = {test: 'test'};
res.writeHead(200, 'reason');
});
@@ -637,7 +642,7 @@ exports['custom path'] = function (test) {
};
sessions({
secret: 'secret',
- timeout: 12345,
+ timeout: 12345000,
path: '/test/path'
})(req, res, function () {
req.session = {test: 'test'};
@@ -657,10 +662,136 @@ exports['don\'t set cookie if incorrect path'] = function (test) {
};
sessions({
secret: 'secret',
- timeout: 12345,
+ timeout: 12345000,
path: '/test/path'
})(req, res, function () {
req.session = {test: 'test'};
res.writeHead(200, {'other_header': 'val'});
});
};
+
+exports['custom domain'] = function (test) {
+ test.expect(2);
+ var req = {headers: {cookie:''}, url: '/'};
+ var res = {
+ writeHead: function (code, headers) {
+ test.equal(code, 200);
+ test.ok(/domain=testdomain.com/.test(headers['Set-Cookie']));
+ test.done();
+ }
+ };
+ sessions({
+ secret: 'secret',
+ domain: 'testdomain.com'
+ })(req, res, function () {
+ req.session = {test: 'test'};
+ res.writeHead(200, {'other_header': 'val'});
+ });
+};
+
+exports['secure'] = function (test) {
+ test.expect(2);
+ var req = {headers: {cookie:''}, url: '/'};
+ var res = {
+ writeHead: function (code, headers) {
+ test.equal(code, 200);
+ test.ok(/secure;/.test(headers['Set-Cookie']));
+ test.done();
+ }
+ };
+ sessions({
+ secret: 'secret',
+ secure: true
+ })(req, res, function () {
+ req.session = {test: 'test'};
+ res.writeHead(200, {'other_header': 'val'});
+ });
+};
+
+exports['useExpires: false'] = function(test){
+ test.expect(2);
+ var _serialize = sessions.serialize;
+ sessions.serialize = function(){
+ return 'session_data';
+ };
+
+ var req = {headers: {cookie:''}, url: '/'};
+ var res = {writeHead: function(statusCode, headers){
+ test.equals(statusCode, 200);
+ test.same(headers, {
+ 'other_header': 'val',
+ 'Set-Cookie': '_node=session_data; ' +
+ 'max-age=12345; ' +
+ 'path=/; HttpOnly'
+ });
+ sessions.serialize = _serialize;
+ test.done();
+ }};
+ sessions({secret: 'secret', timeout: 12345000, useExpires: false})(req, res, function(){
+ req.session = {test: 'test'};
+ res.writeHead(200, {'other_header': 'val'});
+ });
+};
+
+exports['useMaxAge: false'] = function(test){
+ test.expect(3);
+ var _serialize = sessions.serialize;
+ sessions.serialize = function(){
+ return 'session_data';
+ };
+
+ var _expires = sessions.expires;
+ sessions.expires = function(timeout){
+ test.equals(timeout, 12345000);
+ return 'expiry_date';
+ };
+ var req = {headers: {cookie:''}, url: '/'};
+ var res = {writeHead: function(statusCode, headers){
+ test.equals(statusCode, 200);
+ test.same(headers, {
+ 'other_header': 'val',
+ 'Set-Cookie': '_node=session_data; ' +
+ 'expires=expiry_date; ' +
+ 'path=/; HttpOnly'
+ });
+ sessions.serialize = _serialize;
+ sessions.expires = _expires;
+ test.done();
+ }};
+ sessions({secret: 'secret', timeout: 12345000, useMaxAge: false})(req, res, function(){
+ req.session = {test: 'test'};
+ res.writeHead(200, {'other_header': 'val'});
+ });
+};
+
+exports['useHttpOnly: false'] = function(test){
+ test.expect(3);
+ var _serialize = sessions.serialize;
+ sessions.serialize = function(){
+ return 'session_data';
+ };
+
+ var _expires = sessions.expires;
+ sessions.expires = function(timeout){
+ test.equals(timeout, 12345000);
+ return 'expiry_date';
+ };
+ var req = {headers: {cookie:''}, url: '/'};
+ var res = {writeHead: function(statusCode, headers){
+ test.equals(statusCode, 200);
+ test.same(headers, {
+ 'other_header': 'val',
+ 'Set-Cookie': '_node=session_data; ' +
+ 'expires=expiry_date; ' +
+ 'max-age=12345; ' +
+ 'path=/'
+ });
+ sessions.serialize = _serialize;
+ sessions.expires = _expires;
+ test.done();
+ }};
+ sessions({secret: 'secret', timeout: 12345000, useHttpOnly: false})(req, res, function(){
+ req.session = {test: 'test'};
+ res.writeHead(200, {'other_header': 'val'});
+ });
+};
Something went wrong with that request. Please try again.