Skip to content

Loading…

CoffeeScript rewrite + Added an onError setting to the initial call #21

Open
wants to merge 15 commits into from

2 participants

@hiddentao

I started off by merging publickeating's excellent work (#14) and then went onto rewrite the code in CoffeeScript - the code easier to read and it makes it easier to ensure generated code is JSLint compliant. A Cakefile is included to generate the lib/cookie-sessions.js file. I ran all the tests to make sure everything still works as expected.

I then added an onError setting. By default it's null, in which case Errors are thrown as normal. If a function is provided then that function is called instead with the Error object representing the error. Tests have been added for using onError with all existing error messages.

Example for onError:

    sessions({
        ...
        onError: function(err) {
            // e.g. err.toString() -> "Error: Invalid cookie"
        }
    });
publickeating and others 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
@publickeating publickeating Give the error a type so that we can identify properly. 043f2cb
@hiddentao hiddentao Merge remote-tracking branch 'publickeating/master' e054929
@hiddentao hiddentao Wrote CoffeeScript version of code. Added Cakefile.
Updated test code to work with node 0.6.5 (require.paths.push no longer supported). All tests pass.
3b5b5db
@hiddentao hiddentao Added an onError setting to the initial call. If ommitted (default) t…
…hen the cookie-session will throw errors as normal. If instead a function is provided for this setting then that function will recieve a callback with the Error object.
c793ee0
@hiddentao hiddentao Don't export MAX_LENGTH value. dfd508e
@hiddentao hiddentao Get rid of unnecessary 'this' references. d32cb19
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 committed
    …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 committed
    …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 committed
    …milliseconds, not in seconds.
Commits on May 30, 2011
  1. @publickeating

    Whitespace

    publickeating committed
Commits on Jun 9, 2011
  1. @publickeating
Commits on Jan 12, 2012
  1. @hiddentao
  2. @hiddentao

    Wrote CoffeeScript version of code. Added Cakefile.

    hiddentao committed
    Updated test code to work with node 0.6.5 (require.paths.push no longer supported). All tests pass.
Commits on Jan 13, 2012
  1. @hiddentao

    Added an onError setting to the initial call. If ommitted (default) t…

    hiddentao committed
    …hen the cookie-session will throw errors as normal. If instead a function is provided for this setting then that function will recieve a callback with the Error object.
  2. @hiddentao

    Don't export MAX_LENGTH value.

    hiddentao committed
  3. @hiddentao
Showing with 710 additions and 215 deletions.
  1. +1 −0 .gitignore
  2. +16 −0 Cakefile
  3. +188 −193 lib/cookie-sessions.js
  4. +4 −1 package.json
  5. +253 −0 src/cookie-sessions.coffee
  6. +2 −4 test.js
  7. +246 −17 test/test-cookie-sessions.js
View
1 .gitignore
@@ -0,0 +1 @@
+node_modules
View
16 Cakefile
@@ -0,0 +1,16 @@
+# Based on https://github.com/twilson63/express-coffee/blob/master/Cakefile
+
+fs = require 'fs'
+{print} = require 'sys'
+{spawn, exec} = require 'child_process'
+
+build = (callback) ->
+ options = ['-c', '-o', 'lib', 'src']
+ coffee = spawn 'coffee', options
+ coffee.stdout.on 'data', (data) -> print data.toString()
+ coffee.stderr.on 'data', (data) -> print data.toString()
+ coffee.on 'exit', (status) -> callback?() if status is 0
+
+task 'build', 'Compile CoffeeScript source files', ->
+ build()
+
View
381 lib/cookie-sessions.js
@@ -1,218 +1,213 @@
-var crypto = require('crypto');
-var url = require('url');
-
-var 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: '/'
+(function() {
+ var MAX_LENGTH, crypto, exports, url;
+ var __hasProp = Object.prototype.hasOwnProperty;
+
+ crypto = require('crypto');
+
+ url = require('url');
+
+ MAX_LENGTH = 4096;
+
+ exports = module.exports = function(settings) {
+ var k, s, v;
+ s = {
+ session_key: '_node',
+ timeout: 24 * 60 * 60 * 1000,
+ path: '/',
+ domain: null,
+ secure: false,
+ useMaxAge: true,
+ useExpires: true,
+ useHttpOnly: true,
+ onError: null
};
- 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)
- 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) {
- next();
- return;
+ for (k in settings) {
+ if (!__hasProp.call(settings, k)) continue;
+ v = settings[k];
+ s[k] = v;
+ }
+ if ("function" === typeof s.onError) exports.Events.onError = s.onError;
+ if (!s.secret) {
+ return exports.Events.throwErr('No secret set in cookie-session settings');
+ }
+ if ("string" !== typeof s.path || 0 !== s.path.indexOf("/")) {
+ return exports.Events.throwErr('Invalid cookie path, must start with "/"');
+ }
+ return function(req, res, next) {
+ var _writeHead;
+ var _this = this;
+ if (0 !== url.parse(req.url).pathname.indexOf(s.path)) return next();
+ req.session = exports.readSession(s.session_key, s.secret, s.timeout, req);
+ _writeHead = res.writeHead;
+ res.writeHead = function(statusCode) {
+ var args, cookiestr, headers, reasonPhrase, serializedData;
+ reasonPhrase = null;
+ headers = null;
+ if ("string" === typeof arguments[1]) {
+ reasonPhrase = arguments[1];
+ headers = arguments[2] || {};
+ } else {
+ headers = arguments[1] || {};
}
-
- // Read session data from a request and store it in req.session
- req.session = exports.readSession(
- s.session_key, s.secret, s.timeout, req);
-
- // proxy writeHead to add cookie to response
- var _writeHead = res.writeHead;
- res.writeHead = function(statusCode){
-
- var reasonPhrase, headers;
- if (typeof arguments[1] === 'string') {
- reasonPhrase = arguments[1];
- headers = arguments[2] || {};
- }
- else {
- headers = arguments[1] || {};
- }
-
- // Add a Set-Cookie header to all responses with the session data
- // and the current timestamp. The cookie needs to be set on every
- // response so that the timestamp is up to date, and the session
- // does not expire unless the user is inactive.
-
- 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 = null;
+ if (!req.session) {
+ if ("cookie" in req.headers) {
+ cookiestr = escape(s.session_key) + '=';
+ s.timeout = 0;
+ }
+ } else {
+ serializedData = exports.serialize(s.secret, req.session);
+ if (serializedData) {
+ cookiestr = escape(s.session_key) + '=' + escape(serializedData);
+ }
+ }
+ if (cookiestr) {
+ if (s.useExpires) cookiestr += '; expires=' + exports.expires(s.timeout);
+ if (s.useMaxAge) cookiestr += '; max-age=' + (s.timeout / 1000);
+ 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 (headers['Set-Cookie']) {
+ headers = exports.headersToArray(headers);
+ headers.push(['Set-Cookie', cookiestr]);
} else {
- cookiestr = escape(s.session_key) + '='
- + escape(exports.serialize(s.secret, req.session))
- + '; expires=' + exports.expires(s.timeout)
- + '; path=' + s.path + '; HttpOnly';
- }
-
- if (cookiestr !== undefined) {
- 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){
- 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 {
- headers['Set-Cookie'] = cookiestr;
- }
- }
- }
-
- var args = [statusCode, reasonPhrase, headers];
- if (!args[1]) {
- args.splice(1, 1);
+ headers['Set-Cookie'] = cookiestr;
}
- // call the original writeHead on the request
- return _writeHead.apply(res, args);
+ }
}
- next();
+ args = [statusCode, reasonPhrase, headers];
+ if (!args[1]) args.splice(1, 1);
+ return _writeHead.apply(res, args);
+ };
+ return next();
+ };
+ };
+
+ exports.readSession = function(session_key, secret, timeout, req) {
+ var cookies;
+ cookies = exports.readCookies(req);
+ if (session_key in cookies && cookies[session_key]) {
+ return exports.deserialize(secret, timeout, cookies[session_key]);
+ } else {
+
+ }
+ };
+ exports.readCookies = function(req) {
+ var cookie, func, parts;
+ if (req.cookies) {
+ return req.cookies;
+ } else {
+ cookie = req.headers.cookie;
+ if (!cookie) return {};
+ parts = cookie.split(/\s*;\s*/g).map(function(x) {
+ return x.split('=');
+ });
+ func = function(a, x) {
+ a[unescape(x[0])] = unescape(x[1]);
+ return a;
+ };
+ return parts.reduce(func, {});
+ }
+ };
+
+ exports.headersToArray = function(headers) {
+ var func;
+ if (Array.isArray(headers)) return headers;
+ func = function(arr, k) {
+ arr.push([k, headers[k]]);
+ return arr;
};
-};
-
-exports.headersToArray = function(headers){
- if(Array.isArray(headers)) return headers;
- return Object.keys(headers).reduce(function(arr, k){
- arr.push([k, headers[k]]);
- return arr;
- }, []);
-};
-
-
-// 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)){
- throw new Error('invalid cookie');
+ return Object.keys(headers).reduce(func, []);
+ };
+
+ exports.deserialize = function(secret, timeout, str) {
+ var error;
+ if (!exports.valid(secret, timeout, str)) {
+ error = new Error('Invalid cookie');
+ error.type = 'InvalidCookieError';
+ return exports.Events.throwErr(error);
}
- var data = exports.decrypt(secret, exports.split(str).data_blob);
- return JSON.parse(data);
-};
-
-exports.serialize = function(secret, data){
- // Turns a JSON-compatibile object literal into a secure cookie string
-
- var data_str = JSON.stringify(data);
- var data_enc = exports.encrypt(secret, data_str);
- 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)){
- throw new Error('data too long to store in a cookie');
+ return JSON.parse(exports.decrypt(secret, exports.split(str).data_blob));
+ };
+
+ exports.serialize = function(secret, data) {
+ var data_enc, data_str, hmac_sig, result, timestamp;
+ data_str = JSON.stringify(data);
+ data_enc = exports.encrypt(secret, data_str);
+ timestamp = new Date().getTime();
+ hmac_sig = exports.hmac_signature(secret, timestamp, data_enc);
+ result = hmac_sig + timestamp + data_enc;
+ if (!exports.checkLength(result)) {
+ return exports.Events.throwErr('Data too long to store in a cookie');
}
return result;
-};
+ };
-exports.split = function(str){
- // Splits a cookie string into hmac signature, timestamp and data blob.
+ exports.split = function(str) {
return {
- hmac_signature: str.slice(0,40),
- timestamp: parseInt(str.slice(40, 53), 10),
- data_blob: str.slice(53)
+ hmac_signature: str.slice(0, 40),
+ timestamp: parseInt(str.slice(40, 53), 10),
+ data_blob: str.slice(53)
};
-};
+ };
-exports.hmac_signature = function(secret, timestamp, data){
- // Generates a HMAC for the timestamped data, returning the
- // hex digest for the signature.
- var hmac = crypto.createHmac('sha1', secret);
+ exports.hmac_signature = function(secret, timestamp, data) {
+ var hmac;
+ hmac = crypto.createHmac('sha1', secret);
hmac.update(timestamp + data);
return hmac.digest('hex');
-};
-
-exports.valid = function(secret, timeout, str){
- // Tests the validity of a cookie string. Returns true if the HMAC
- // signature of the secret, timestamp and data blob matches the HMAC in the
- // cookie string, and the cookie's age is less than the timeout value.
-
- var parts = exports.split(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()
- );
-};
-
-exports.decrypt = function(secret, str){
- // Decrypt the aes192 encoded str using secret.
- var decipher = crypto.createDecipher("aes192", secret);
+ };
+
+ exports.valid = function(secret, timeout, str) {
+ var hmac_sig, parts;
+ parts = exports.split(str);
+ hmac_sig = exports.hmac_signature(secret, parts.timestamp, parts.data_blob);
+ return parts.hmac_signature === hmac_sig && new Date().getTime() < (parts.timestamp + timeout);
+ };
+
+ exports.decrypt = function(secret, str) {
+ var decipher;
+ decipher = crypto.createDecipher("aes192", secret);
return decipher.update(str, 'hex', 'utf8') + decipher.final('utf8');
-};
+ };
-exports.encrypt = function(secret, str){
- // Encrypt the str with aes192 using secret.
- var cipher = crypto.createCipher("aes192", secret);
+ exports.encrypt = function(secret, str) {
+ var cipher;
+ cipher = crypto.createCipher("aes192", secret);
return cipher.update(str, 'utf8', 'hex') + cipher.final('hex');
-};
+ };
-exports.checkLength = function(str){
- // Test if a string is within the maximum length allowed for a cookie.
- return str.length <= 4096;
-};
+ exports.checkLength = function(str) {
+ return str.length <= MAX_LENGTH;
+ };
-exports.readCookies = function(req){
- // if "cookieDecoder" is in use, then req.cookies
- // 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 {};
- }
- 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.expires = function(timeout) {
+ return new Date(new Date().getTime() + timeout).toUTCString();
+ };
-exports.readSession = function(key, secret, timeout, req){
- // Reads the session data stored in the cookie named 'key' if it validates,
- // otherwise returns an empty object.
+ exports.Events = (function() {
- var cookies = exports.readCookies(req);
- if(cookies[key]){
- return exports.deserialize(secret, timeout, cookies[key]);
- }
- return undefined;
-};
+ function Events() {}
+
+ Events.onError = null;
+
+ Events.throwErr = function(err) {
+ if (typeof err !== "object") err = new Error(err);
+ if (this.onError) {
+ this.onError(err);
+ } else {
+ throw err;
+ }
+ return;
+ };
+
+ return Events;
+ })();
-exports.expires = function(timeout){
- return (new Date(new Date().getTime() + (timeout))).toUTCString();
-};
+}).call(this);
View
5 package.json
@@ -5,7 +5,7 @@
, "version": "0.0.2"
, "repository" :
{ "type" : "git"
- , "url" : "http://github.com/caolan/cookie-sessions.git"
+ , "url" : "http://github.com/caolan/cookie-sessions.git"
}
, "bugs" : { "url" : "http://github.com/caolan/cookie-sessions/issues" }
, "licenses" :
@@ -13,4 +13,7 @@
, "url" : "http://github.com/caolan/cookie-sessions/raw/master/LICENSE"
}
]
+, "devDependencies": {
+ "coffee-script": "1.1.x"
+ }
}
View
253 src/cookie-sessions.coffee
@@ -0,0 +1,253 @@
+crypto = require('crypto')
+url = require('url')
+
+# max allowable length of a cookie (4KB)
+MAX_LENGTH = 4096
+
+
+exports = module.exports = (settings) ->
+ s =
+ # key name
+ session_key: '_node'
+ # timeout/expiry (24 hours)
+ timeout: 24 * 60 * 60 * 1000
+ # path
+ path: '/'
+ # domain
+ domain: null
+ # https only?
+ secure: false
+ # use 'max-age' ?
+ useMaxAge: true
+ # use 'expires'?
+ useExpires: true
+ # use 'HttpOnly'?
+ useHttpOnly: true
+ # error handler
+ onError: null
+
+ # extend with passed-in settings
+ for own k,v of settings
+ s[k] = v
+
+ # do we have an error handling callback?
+ if "function" is typeof s.onError
+ exports.Events.onError = s.onError
+
+ # do some basic checks
+ if not s.secret
+ return exports.Events.throwErr 'No secret set in cookie-session settings'
+
+ if "string" isnt typeof s.path or 0 isnt s.path.indexOf("/")
+ return exports.Events.throwErr 'Invalid cookie path, must start with "/"'
+
+
+ # Handle a request - the main method!
+ return (req, res, next) ->
+ # if the request is not under the specified path, do nothing.
+ if 0 isnt url.parse(req.url).pathname.indexOf(s.path)
+ return next()
+
+ # Read session data from a request and store it in req.session
+ req.session = exports.readSession(s.session_key, s.secret, s.timeout, req)
+
+ # proxy writeHead to add cookie to response
+ _writeHead = res.writeHead
+ res.writeHead = (statusCode) =>
+
+ reasonPhrase = null
+ headers = null
+ if "string" is typeof arguments[1]
+ reasonPhrase = arguments[1]
+ headers = arguments[2] or {}
+ else
+ headers = arguments[1] or {}
+
+
+ # Add a Set-Cookie header to all responses with the session data
+ # and the current timestamp. The cookie needs to be set on every
+ # response so that the timestamp is up to date, and the session
+ # does not expire unless the user is inactive.
+
+ cookiestr = null
+ # no session yet
+ if not req.session
+ if "cookie" of req.headers
+ cookiestr = escape(s.session_key) + '='
+ s.timeout = 0
+ else
+ serializedData = exports.serialize(s.secret, req.session)
+ if serializedData
+ cookiestr = escape(s.session_key) + '=' + escape(serializedData)
+
+ if cookiestr
+ if s.useExpires then cookiestr += '; expires=' + exports.expires(s.timeout)
+ if s.useMaxAge then cookiestr += '; max-age=' + (s.timeout / 1000) # In seconds
+ if s.path then cookiestr += '; path=' + s.path
+ if s.domain then cookiestr += '; domain=' + s.domain
+ if s.secure then cookiestr += '; secure'
+ if s.useHttpOnly then 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']
+ headers = exports.headersToArray(headers)
+ headers.push ['Set-Cookie', cookiestr]
+ else
+ # if no Set-Cookie header exists, leave the headers as an
+ # object, and add a Set-Cookie property
+ headers['Set-Cookie'] = cookiestr
+
+
+ args = [statusCode, reasonPhrase, headers]
+ # get rid of reasonPhrase if not defined
+ if not args[1]
+ args.splice(1, 1)
+
+ # call the original writeHead on the request
+ return _writeHead.apply res, args
+
+ next()
+
+
+
+
+# read session from given request cookie
+# @return undefined if session data wasn't found or couldn't be read
+exports.readSession = (session_key, secret, timeout, req) ->
+ # Reads the session data stored in the cookie named 'key' if it validates,
+ # otherwise returns an empty object.
+ cookies = exports.readCookies(req)
+ if session_key of cookies and cookies[session_key]
+ return exports.deserialize(secret, timeout, cookies[session_key])
+ else
+ return undefined;
+
+
+# read cookies from request object
+exports.readCookies = (req) ->
+ # if "cookieDecoder" is in use, then req.cookies
+ # will already contain the parsed cookies
+ if req.cookies
+ return req.cookies
+ else
+ # Extracts the cookies from a request object.
+ cookie = req.headers.cookie
+ return {} if not cookie
+
+ parts = cookie.split(/\s*;\s*/g).map (x) ->
+ x.split('=')
+
+ func = (a, x) ->
+ a[unescape(x[0])] = unescape(x[1])
+ a
+
+ parts.reduce(func, {})
+
+
+
+# convert key-value headers into arrays
+exports.headersToArray = (headers) ->
+ return headers if Array.isArray(headers)
+ func = (arr, k) ->
+ arr.push [k, headers[k]]
+ arr
+ Object.keys(headers).reduce(func, [])
+
+
+# parse cookie data
+# @return undefined if data couldn't be parsed
+exports.deserialize = (secret, timeout, str) ->
+ # Parses a secure cookie string, returning the object stored within it.
+ # Returns undefined (and sends out an error) if the secure cookie string does not validate.
+ if not exports.valid(secret, timeout, str)
+ error = new Error('Invalid cookie')
+ error.type = 'InvalidCookieError'
+ return exports.Events.throwErr(error)
+ JSON.parse(exports.decrypt(secret, exports.split(str).data_blob))
+
+
+# construct cookie data
+# @return undefined if data couldn't be constructed
+exports.serialize = (secret, data) ->
+ # Turns a JSON-compatibile object literal into a secure cookie string
+ data_str = JSON.stringify(data)
+ data_enc = exports.encrypt(secret, data_str)
+ timestamp = new Date().getTime()
+ hmac_sig = exports.hmac_signature(secret, timestamp, data_enc)
+ result = hmac_sig + timestamp + data_enc
+ if not exports.checkLength(result)
+ return exports.Events.throwErr 'Data too long to store in a cookie'
+ result
+
+
+exports.split = (str) ->
+ # Splits a cookie string into hmac signature, timestamp and data blob.
+ return {
+ hmac_signature: str.slice(0,40)
+ timestamp: parseInt(str.slice(40, 53), 10)
+ data_blob: str.slice(53)
+ }
+
+
+# calculate hmac signature
+exports.hmac_signature = (secret, timestamp, data) ->
+ # Generates a HMAC for the timestamped data, returning the
+ # hex digest for the signature.
+ hmac = crypto.createHmac('sha1', secret)
+ hmac.update(timestamp + data);
+ hmac.digest('hex')
+
+
+exports.valid = (secret, timeout, str) ->
+ # Tests the validity of a cookie string. Returns true if the HMAC
+ # signature of the secret, timestamp and data blob matches the HMAC in the
+ # cookie string, and the cookie's age is less than the timeout value.
+ parts = exports.split(str)
+ hmac_sig = exports.hmac_signature secret, parts.timestamp, parts.data_blob
+ return parts.hmac_signature is hmac_sig and
+ new Date().getTime() < (parts.timestamp + timeout)
+
+
+exports.decrypt = (secret, str) ->
+ # Decrypt the aes192 encoded str
+ decipher = crypto.createDecipher("aes192", secret)
+ decipher.update(str, 'hex', 'utf8') + decipher.final('utf8')
+
+
+exports.encrypt = (secret, str) ->
+ # Encrypt the str with aes192 using secret.
+ cipher = crypto.createCipher("aes192", secret)
+ cipher.update(str, 'utf8', 'hex') + cipher.final('hex')
+
+
+exports.checkLength = (str) ->
+ # Test if a string is within the maximum length allowed for a cookie (4KB.
+ return str.length <= MAX_LENGTH
+
+
+# Generates an expires date
+# exports.params timeout the time in milliseconds before the cookie expires
+exports.expires = (timeout) ->
+ new Date(new Date().getTime() + timeout).toUTCString();
+
+
+# events delegate
+class exports.Events
+ # The error-handling callback
+ @onError: null
+
+ # Throw an error
+ # @param errObj an Error object or error message
+ # @return undefined
+ @throwErr: (err) ->
+ if typeof err isnt "object"
+ err = new Error(err)
+ if @onError then @onError(err) else throw err
+ undefined
+
+
+
View
6 test.js
@@ -1,11 +1,9 @@
#!/usr/local/bin/node
-require.paths.push(__dirname);
-require.paths.push(__dirname + '/deps');
-require.paths.push(__dirname + '/lib');
+var nodeunit = require('./deps/nodeunit');
try {
- var testrunner = require('nodeunit').testrunner;
+ var testrunner = nodeunit.reporters.default;
}
catch(e) {
var sys = require('sys');
View
263 test/test-cookie-sessions.js
@@ -1,4 +1,4 @@
-var sessions = require('cookie-sessions');
+var sessions = require('../lib/cookie-sessions');
exports['split'] = function(test){
@@ -206,7 +206,7 @@ exports['serialize data over 4096 chars'] = function(test){
var r = '';
for(var i=0; i<4089; i++){
r = r + 'x';
- };
+ }
return r;
};
sessions.hmac_signature = function(secret, timestamp, data_str){
@@ -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,234 @@ 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'});
+ });
+};
+
+
+exports['onError: no secret'] = function(test){
+ test.expect(2);
+ sessions.Events.onError = null;
+ var errMsg = "Error: No secret set in cookie-session settings";
+
+ try {
+ sessions();
+ } catch (err) {
+ test.equals(err.toString(), errMsg)
+ }
+ sessions({
+ onError: function(err) {
+ test.equals(err.toString(), errMsg);
+ test.done();
+ }
+ });
+};
+
+
+exports['onError: bad path'] = function(test){
+ test.expect(2);
+ sessions.Events.onError = null;
+ var errMsg = "Error: Invalid cookie path, must start with \"/\"";
+
+ try {
+ sessions({
+ secret: "test",
+ path: "o"
+ });
+ } catch (err) {
+ test.equals(err.toString(), errMsg)
+ }
+
+ sessions({
+ secret: "test",
+ path: "o",
+ onError: function(err) {
+ test.equals(err.toString(), errMsg);
+ test.done();
+ }
+ });
+};
+
+
+
+exports['onError: bad cookie string'] = function(test){
+ test.expect(4);
+ sessions.Events.onError = null;
+ var errMsg = "Error: Invalid cookie";
+
+ try {
+ sessions.deserialize("test", 100, "blabla");
+ } catch (err) {
+ test.equals(err.type, "InvalidCookieError");
+ test.equals(err.toString(), errMsg)
+ }
+
+ sessions({
+ secret: "test",
+ onError: function (err) {
+ test.equals(err.type, "InvalidCookieError");
+ test.equals(err.toString(), errMsg);
+ test.done();
+ }
+ });
+ sessions.deserialize("test", 100, "blabla");
+};
+
+
+
+exports['onError: data too long'] = function(test){
+ test.expect(2);
+ sessions.Events.onError = null;
+ var errMsg = "Error: Data too long to store in a cookie";
+
+ var data = '';
+ for(var i=0; i<4089; i++){
+ data = data + 'x';
+ }
+
+
+ try {
+ sessions.serialize("test", data);
+ } catch (err) {
+ test.equals(err.toString(), errMsg)
+ }
+
+ sessions({
+ secret: "test",
+ onError: function (err) {
+ test.equals(err.toString(), errMsg);
+ test.done();
+ }
+ });
+ sessions.serialize("test", data);
+};
Something went wrong with that request. Please try again.