diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/Cakefile b/Cakefile new file mode 100644 index 0000000..0e40e49 --- /dev/null +++ b/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() + diff --git a/lib/cookie-sessions.js b/lib/cookie-sessions.js index bc14262..839675e 100644 --- a/lib/cookie-sessions.js +++ b/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); diff --git a/package.json b/package.json index daddbdb..66fe4f3 100644 --- a/package.json +++ b/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" + } } diff --git a/src/cookie-sessions.coffee b/src/cookie-sessions.coffee new file mode 100644 index 0000000..b08beba --- /dev/null +++ b/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 + + + diff --git a/test.js b/test.js index f41d154..2936f2b 100755 --- a/test.js +++ b/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'); diff --git a/test/test-cookie-sessions.js b/test/test-cookie-sessions.js index c43dc87..d1af212 100644 --- a/test/test-cookie-sessions.js +++ b/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); +}; \ No newline at end of file