diff --git a/README.md b/README.md index 667ddc1b299a823b015ea5f775ff3b96edbe37ce..958a2579ac4e7db28f191c157a28fb4ebcaeb004 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ it go. [npm](http://npmjs.org/) is a good tool to use to acquire the requisite * xml2js (>= 0.1.5) * sqlite (>= 1.0.3) * mustache (>= 0.3.1) -* cookie-sessions (>= 0.0.2) +* cookie-sessions (patched version included in-tree, nothing to be done) ## Getting started: diff --git a/authority/server/run.js b/authority/server/run.js index 73816d6f5c457e00e8e2ea0e8d79b5717aad9f5a..ea8bfa97f78054fbeaa3166c8ce1db1037a1e060 100644 --- a/authority/server/run.js +++ b/authority/server/run.js @@ -1,9 +1,10 @@ -const path = require('path'), - url = require('url'), - wsapi = require('./wsapi.js'), - httputils = require('./httputils.js'), - connect = require('connect'), - webfinger = require('./webfinger.js'); +const path = require('path'), + url = require('url'), + wsapi = require('./wsapi.js'), + httputils = require('./httputils.js'), + connect = require('connect'), + webfinger = require('./webfinger.js'), + sessions = require('cookie-sessions'); const STATIC_DIR = path.join(path.dirname(__dirname), "static"); @@ -40,16 +41,9 @@ exports.handler = function(request, response, serveFile) { exports.setup = function(server) { var week = (7 * 24 * 60 * 60 * 1000); - - server - .use(connect.cookieParser()) - .use(connect.session({ - secret: "mouse dog", - cookie: { - path: '/wsapi', - httpOnly: true, - expires: new Date(Date.now() + week),// a week XXX: think about session security, etc - maxAge: week - } - })); + server.use(sessions({ + secret: 'v3wy s3kr3t', + session_key: "browserid_state", + path: '/' + })); } diff --git a/authority/server/wsapi.js b/authority/server/wsapi.js index 1dc316de155b6049e64d15df1618db98f18ed75d..dbe09145cb195f9a186ef9512431ce81e29d3ea1 100644 --- a/authority/server/wsapi.js +++ b/authority/server/wsapi.js @@ -24,8 +24,12 @@ function checkParams(getArgs, resp, params) { return true; } -function isAuthed(req, resp) { - if (typeof req.session.authenticatedUser !== 'string') { +function isAuthed(req) { + return (req.session && typeof req.session.authenticatedUser === 'string'); +} + +function checkAuthed(req, resp) { + if (!isAuthed(req)) { httputils.badRequest(resp, "requires authentication"); return false; } @@ -59,11 +63,13 @@ exports.stage_user = function(req, resp) { // upon success, stage_user returns a secret (that'll get baked into a url // and given to the user), on failure it throws var secret = db.stageUser(getArgs); - httputils.jsonResponse(resp, true); // store the email being registered in the session data + if (!req.session) req.session = {}; req.session.pendingRegistration = getArgs.email; + httputils.jsonResponse(resp, true); + // let's now kick out a verification email! email.sendVerificationEmail(getArgs.email, secret); } catch(e) { @@ -75,6 +81,13 @@ exports.stage_user = function(req, resp) { exports.registration_status = function(req, resp) { logRequest("registration_status", req.session); + if (!req.session || !typeof req.session.pendingRegistration == 'string') { + httputils.badRequest( + resp, + "api abuse: registration_status called without a pending email for registration"); + return; + } + var email = req.session.pendingRegistration; db.emailKnown(email, function(known) { if (known) { @@ -96,7 +109,10 @@ exports.authenticate_user = function(req, resp) { if (!checkParams(getArgs, resp, [ "email", "pass" ])) return; db.checkAuth(getArgs.email, getArgs.pass, function(rv) { - if (rv) req.session.authenticatedUser = getArgs.email; + if (rv) { + if (!req.session) req.session = {}; + req.session.authenticatedUser = getArgs.email; + } httputils.jsonResponse(resp, rv); }); }; @@ -107,7 +123,7 @@ exports.add_email = function (req, resp) { if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return; - if (!isAuthed(req, resp)) return; + if (!checkAuthed(req, resp)) return; logRequest("add_email", getArgs); @@ -115,11 +131,12 @@ exports.add_email = function (req, resp) { // upon success, stage_user returns a secret (that'll get baked into a url // and given to the user), on failure it throws var secret = db.stageEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey); - httputils.jsonResponse(resp, true); // store the email being registered in the session data req.session.pendingRegistration = getArgs.email; + httputils.jsonResponse(resp, true); + // let's now kick out a verification email! email.sendVerificationEmail(getArgs.email, secret); } catch(e) { @@ -132,7 +149,7 @@ exports.set_key = function (req, resp) { var urlobj = url.parse(req.url, true); var getArgs = urlobj.query; if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return; - if (!isAuthed(req, resp)) return; + if (!checkAuthed(req, resp)) return; logRequest("set_key", getArgs); db.addKeyToEmail(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) { httputils.jsonResponse(resp, rv); @@ -145,7 +162,7 @@ exports.am_authed = function(req,resp) { }; exports.sync_emails = function(req,resp) { - if (!isAuthed(req, resp)) return; + if (!checkAuthed(req, resp)) return; var requestBody = ""; req.on('data', function(str) { diff --git a/authority/static/dialog/main.js b/authority/static/dialog/main.js index 0357e73de89e07e046b5d08838c826f6406048dc..0d6058fbf267cb27f7da9a57cc51aebd7a8b27e3 100644 --- a/authority/static/dialog/main.js +++ b/authority/static/dialog/main.js @@ -587,18 +587,23 @@ window.localStorage.emails = JSON.stringify({}); } + function onsuccess(rv) { + trans.complete(rv); + } + function onerror(error) { + errorOut(trans, error); + } + + // wherever shall we start? if (haveIDs) { - runSignInDialog(function(rv) { - trans.complete(rv); - }, function(error) { - errorOut(trans, error); - }); + runSignInDialog(onsuccess, onerror); } else { - runAuthenticateDialog(undefined, function(rv) { - trans.complete(rv); - }, function(error) { - errorOut(trans, error); - }); + // do we even need to authenticate? + checkAuthStatus(function() { + syncIdentities(onsuccess, onerror); + }, function() { + runAuthenticateDialog(onsuccess, onerror); + }, onsuccess, onerror); } }); diff --git a/node_modules/README.md b/node_modules/README.md new file mode 100644 index 0000000000000000000000000000000000000000..71a00cdf9da41b8a06c9f41f69302afd3e5e86cc --- /dev/null +++ b/node_modules/README.md @@ -0,0 +1,5 @@ +Here live first and third party re-usable modules for node. + +* cookie-sessions - encrypted cookie based session storage (will a less stateful + server make), forked and improved, thus included inline here. Original work: + https://github.com/caolan/cookie-sessions diff --git a/node_modules/cookie-sessions/.gitmodules b/node_modules/cookie-sessions/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..3b655320c9768cf0a1db8bfc65e3cc4ef6b685ff --- /dev/null +++ b/node_modules/cookie-sessions/.gitmodules @@ -0,0 +1,3 @@ +[submodule "deps/nodeunit"] + path = deps/nodeunit + url = git://github.com/caolan/nodeunit.git diff --git a/node_modules/cookie-sessions/LICENSE b/node_modules/cookie-sessions/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b7f9d5001c0b8d12720085c7982321910515cbe3 --- /dev/null +++ b/node_modules/cookie-sessions/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Caolan McMahon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node_modules/cookie-sessions/README.md b/node_modules/cookie-sessions/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9ba215e563011a19f63615f283b93dce8e50e51b --- /dev/null +++ b/node_modules/cookie-sessions/README.md @@ -0,0 +1,47 @@ +# Cookie-Sessions + +Secure cookie-based session middleware for +[Connect](http://github.com/senchalabs/connect). This is a new module and I +wouldn't recommend for production use just yet. + +Session data is stored on the request object in the 'session' property: + + var connect = require('connect'), + sessions = require('cookie-sessions'); + + Connect.createServer( + sessions({secret: '123abc'}), + function(req, res, next){ + req.session = {'hello':'world'}; + res.writeHead(200, {'Content-Type':'text/plain'}); + res.end('session data updated'); + } + ).listen(8080); + +The session data is JSON.stringified, encrypted and timestamped, then a HMAC +signature is attached to test for tampering. The main function accepts a +number of options: + + * secret -- The secret to encrypt the session data with + * timeout -- The amount of time in miliseconds before the cookie expires + (default: 24 hours) + * session_key -- The cookie key name to store the session data in + (default: _node) + + +## Why store session data in cookies? + +* Its fast, you don't need to hit the filesystem or a database to look up + session data +* It scales easily. You don't need to worry about sticky-sessions when + load-balancing across multiple nodes. +* No server-side persistence requirements + +## Caveats + +* You can only store 4k of data in a cookie +* Higher-bandwidth requirements, since the cookie is sent to the server with + every request. + +__In summary:__ don't use cookie storage if you keep a lot of data in your +sessions! diff --git a/node_modules/cookie-sessions/index.js b/node_modules/cookie-sessions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..888d419abbb0493bb583c72c2d27d9d2f2c4bbcb --- /dev/null +++ b/node_modules/cookie-sessions/index.js @@ -0,0 +1,3 @@ +// This file is just added for convenience so this repository can be +// directly checked out into a project's deps folder +module.exports = require('./lib/cookie-sessions'); diff --git a/node_modules/cookie-sessions/lib/cookie-sessions.js b/node_modules/cookie-sessions/lib/cookie-sessions.js new file mode 100644 index 0000000000000000000000000000000000000000..68b515a9c8bce9866610beec5d3938c5143de723 --- /dev/null +++ b/node_modules/cookie-sessions/lib/cookie-sessions.js @@ -0,0 +1,218 @@ +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: '/' + }; + 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; + } + + // 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; + } + } else { + cookiestr = escape(s.session_key) + '=' + + escape(exports.serialize(s.secret, req.session)) + + '; expires=' + exports.expires(s.timeout) + + '; path=' + s.path; + } + + 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); + } + // call the original writeHead on the request + return _writeHead.apply(res, args); + } + next(); + + }; +}; + +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'); + } + 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 result; +}; + +exports.split = function(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) + }; +}; + +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); + 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); + 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); + 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.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.readSession = function(key, secret, timeout, req){ + // Reads the session data stored in the cookie named 'key' if it validates, + // otherwise returns an empty object. + + var cookies = exports.readCookies(req); + if(cookies[key]){ + return exports.deserialize(secret, timeout, cookies[key]); + } + return undefined; +}; + + +exports.expires = function(timeout){ + return (new Date(new Date().getTime() + (timeout))).toUTCString(); +}; diff --git a/node_modules/cookie-sessions/package.json b/node_modules/cookie-sessions/package.json new file mode 100644 index 0000000000000000000000000000000000000000..374ff1a76ab790df27e89cad8094950e93a6001b --- /dev/null +++ b/node_modules/cookie-sessions/package.json @@ -0,0 +1,16 @@ +{ "name": "cookie-sessions" +, "description": "Secure cookie-based session middleware for Connect" +, "main": "./index" +, "author": "Caolan McMahon" +, "version": "0.0.3" +, "repository" : + { "type" : "git" + , "url" : "http://github.com/caolan/cookie-sessions.git" + } +, "bugs" : { "web" : "http://github.com/caolan/cookie-sessions/issues" } +, "licenses" : + [ { "type" : "MIT" + , "url" : "http://github.com/caolan/cookie-sessions/raw/master/LICENSE" + } + ] +} diff --git a/node_modules/cookie-sessions/test.js b/node_modules/cookie-sessions/test.js new file mode 100755 index 0000000000000000000000000000000000000000..f41d154e3cf5dca054783c3abdb1588184768756 --- /dev/null +++ b/node_modules/cookie-sessions/test.js @@ -0,0 +1,22 @@ +#!/usr/local/bin/node + +require.paths.push(__dirname); +require.paths.push(__dirname + '/deps'); +require.paths.push(__dirname + '/lib'); + +try { + var testrunner = require('nodeunit').testrunner; +} +catch(e) { + var sys = require('sys'); + sys.puts("Cannot find nodeunit module."); + sys.puts("You can download submodules for this project by doing:"); + sys.puts(""); + sys.puts(" git submodule init"); + sys.puts(" git submodule update"); + sys.puts(""); + process.exit(); +} + +process.chdir(__dirname); +testrunner.run(['test']); diff --git a/node_modules/cookie-sessions/test/test-cookie-sessions.js b/node_modules/cookie-sessions/test/test-cookie-sessions.js new file mode 100644 index 0000000000000000000000000000000000000000..432a8ad93dc42740d377392706bd0fc75b2e4eb5 --- /dev/null +++ b/node_modules/cookie-sessions/test/test-cookie-sessions.js @@ -0,0 +1,620 @@ +var sessions = require('cookie-sessions'); + + +exports['split'] = function(test){ + var hmac_sig = 'c82d1eacb4adb15a3250a6df7c8f190b586ab6b9', + timestamp = 1264710287440, + data_blob = 'somedata'; + + var serialized_cookie = hmac_sig + timestamp + data_blob; + test.same( + sessions.split(serialized_cookie), + {hmac_signature: hmac_sig, timestamp: timestamp, data_blob: data_blob}, + 'split correctly seperates sig, timestamp and data blob' + ); + test.done(); +}; + +exports['valid'] = function(test){ + var secret = 'secret'; + current_valid_sig = '5eaaa22480acefd8b18d67bb194573dc1b75d9db', + expired_valid_sig = '9c7ad86913ceeced1f6f249ba52868006c8dfdab', + invalid_sig = '51a2a32485a6e7d8b9810711112513d14b15d16b', + expired_timestamp = 1264700000000, + current_timestamp = 1264710287440, + session_timeout = 54000, + data_blob = 'somedata'; + + var Date_copy = global.Date; + global.Date = function(){this.getTime = function(){return 1264710287000}}; + + test.ok( + sessions.valid( + secret, session_timeout, + current_valid_sig + current_timestamp + data_blob + ) === true, + 'returns true for valid hmac sig within timeout' + ); + test.ok( + sessions.valid( + secret, session_timeout, + expired_valid_sig + expired_timestamp + data_blob + ) === false, + 'returns false for valid hmac sig past timeout' + ); + test.ok( + sessions.valid( + secret, session_timeout, + invalid_sig + current_timestamp + data_blob + ) === false, + 'returns false for invalid hmac sig within timeout' + ); + test.ok( + sessions.valid( + secret, session_timeout, + invalid_sig + expired_timestamp + data_blob + ) === false, + 'returns false for invalid hmac sig past timeout' + ); + + // restore Date + global.Date = Date_copy; + test.done(); +}; + +exports['decrypt'] = function(test){ + var r = sessions.decrypt( + 'secret', '686734eb9e0fff9adea53983210825ef' + ); + test.same(r, 'somedata', 'decrypt sucessfully returns decrypted data'); + test.done(); +}; + +exports['encrypt'] = function(test){ + var r = sessions.encrypt('secret', 'somedata'); + test.same( + r, '686734eb9e0fff9adea53983210825ef', + 'encrypt sucessfully returns encrypted data' + ); + test.done(); +}; + +exports['deserialize valid cookie'] = function(test){ + test.expect(8); + // copy some functions + var valid = sessions.valid; + var decrypt = sessions.decrypt; + var parse = JSON.parse; + + sessions.valid = function(secret, timeout, str){ + test.equals(secret, 'secret', 'valid called with secret'); + test.equals(timeout, 123, 'valid called with timeout'); + test.equals(str, 'cookiestring', 'valid called with cookie string'); + return true; + }; + sessions.split = function(str){ + test.equals(str, 'cookiestring', 'split called with cookie string'); + return {data_blob: 'datastr'}; + }; + sessions.decrypt = function(secret, str){ + test.equals(secret, 'secret', 'decrypt called with secret'); + test.equals(str, 'datastr', 'decrypt called with data string'); + return 'decrypted_data'; + }; + JSON.parse = function(str){ + test.equals( + str, 'decrypted_data', 'JSON.parse called with decrypted data' + ); + return {test:'test'}; + }; + var r = sessions.deserialize('secret', 123, 'cookiestring'); + test.same(r, {test:'test'}, 'deserialize returns parsed json data'); + + // restore copied functions: + sessions.valid = valid; + sessions.decrypt = decrypt; + JSON.parse = parse; + test.done(); +}; + +exports['deserialize invalid cookie'] = function(test){ + test.expect(1); + // copy some functions + var valid = sessions.valid; + var decrypt = sessions.decrypt; + var parse = JSON.parse; + + sessions.valid = function(secret, timeout, str){ + return false; + }; + sessions.decrypt = function(secret, str){ + test.ok(false, 'should not attempt to decrypt invalid cookie'); + }; + JSON.parse = function(str){ + test.ok(false, 'should not attempt to parse invalid cookie'); + }; + try { + sessions.deserialize('secret', 123, 'cookiestring'); + } + catch(e){ + test.ok(true, 'throw exception on invalid cookie'); + } + + // restore copied functions: + sessions.valid = valid; + sessions.decrypt = decrypt; + JSON.parse = parse; + test.done(); +}; + +exports['serialize'] = function(test){ + test.expect(7); + // copy some functions + var encrypt = sessions.encrypt; + var hmac_signature = sessions.hmac_signature; + var stringify= JSON.stringify; + var Date_copy = global.Date; + + global.Date = function(){this.getTime = function(){return 1234;}}; + JSON.stringify = function(obj){ + test.same( + obj, {test:'test'}, 'JSON.stringify called with cookie data obj' + ); + return 'data'; + }; + sessions.encrypt = function(secret, str){ + test.equals(secret, 'secret', 'encrypt called with secret'); + test.equals(str, 'data', 'encrypt called with stringified data'); + return 'encrypted_data'; + }; + sessions.hmac_signature = function(secret, timestamp, data_str){ + test.equals(secret, 'secret', 'hmac_signature called with secret'); + test.equals(timestamp, 1234, 'hmac_signature called with timestamp'); + test.equals( + data_str, 'encrypted_data', + 'hmac_signature called with encrypted data string' + ); + return 'hmac'; + }; + var r = sessions.serialize('secret', {test:'test'}); + test.equals( + r, 'hmac1234encrypted_data', 'serialize returns correct string' + ); + + // restore copied functions: + sessions.encrypt = encrypt; + sessions.hmac_signature = hmac_signature; + JSON.stringify = stringify; + global.Date = Date_copy; + test.done(); +}; + +exports['serialize data over 4096 chars'] = function(test){ + test.expect(1); + // copy some functions + var encrypt = sessions.encrypt; + var hmac_signature = sessions.hmac_signature; + var stringify= JSON.stringify; + var Date_copy = global.Date; + + global.Date = function(){this.getTime = function(){return 1234;}}; + JSON.stringify = function(obj){ + return 'data'; + }; + sessions.encrypt = function(secret, str){ + // lets make this too long! + var r = ''; + for(var i=0; i<4089; i++){ + r = r + 'x'; + }; + return r; + }; + sessions.hmac_signature = function(secret, timestamp, data_str){ + return 'hmac'; + }; + try { + var r = sessions.serialize('secret', {test:'test'}); + } + catch(e){ + test.ok( + true, 'serializing a cookie over 4096 chars throws an exception' + ); + } + + // restore copied functions: + sessions.encrypt = encrypt; + sessions.hmac_signature = hmac_signature; + JSON.stringify = stringify; + global.Date = Date_copy; + test.done(); +}; + +exports['readCookies'] = function(test){ + var req = {headers: {cookie: "name1=data1; test=\"abcXYZ%20123\""}}; + var r = sessions.readCookies(req); + test.same(r, {name1: 'data1', test: '"abcXYZ 123"'}, 'test header read ok'); + test.done(); +}; + +exports['readCookies alternate format'] = function(test){ + var req = {headers: {cookie: "name1=data1;test=\"abcXYZ%20123\""}}; + var r = sessions.readCookies(req); + test.same(r, {name1: 'data1', test: '"abcXYZ 123"'}, 'test header read ok'); + test.done(); +}; + +exports['readCookies no cookie in headers'] = function(test){ + var req = {headers: {}}; + var r = sessions.readCookies(req); + test.same(r, {}, 'returns empty object'); + test.done(); +}; + +exports['readCookies from Connect cookieDecoder'] = function(test){ + var req = {headers: {}, cookies: {'test':'cookie'}}; + test.same(sessions.readCookies(req), {'test': 'cookie'}); + test.done(); +}; + +exports['readSession'] = function(test){ + test.expect(5); + var readCookies = sessions.readCookies; + var deserialize = sessions.deserialize; + + sessions.readCookies = function(r){ + test.equals(r, 'request_obj', 'readCookies called with request object'); + return {'node_session': 'cookie_data'}; + }; + sessions.deserialize = function(secret, timeout, str){ + test.equals(secret, 'secret', 'readCookies called with secret'); + test.equals(timeout, 12, 'readCookies called with timeout'); + test.equals(str, 'cookie_data', 'readCookies called with cookie data'); + return {test: 'test'}; + }; + + var r = sessions.readSession( + 'node_session', 'secret', 12, 'request_obj' + ); + test.same(r, {test: 'test'}, 'session with key node_session read ok'); + + // restore copied functions + sessions.readCookies = readCookies; + sessions.deserialize = deserialize; + test.done(); +}; + +exports['readSession no cookie'] = function(test){ + test.expect(2); + var readCookies = sessions.readCookies; + var deserialize = sessions.deserialize; + + sessions.readCookies = function(r){ + test.equals(r, 'request_obj', 'readCookies called with request object'); + return {}; + }; + sessions.deserialize = function(secret, timeout, str){ + test.ok(false, 'should not call deserialize'); + }; + + var r = sessions.readSession( + 'node_session', 'secret', 12, 'request_obj' + ); + test.same(r, undefined, 'return empty session'); + + // restore copied functions + sessions.readCookies = readCookies; + sessions.deserialize = deserialize; + test.done(); +}; + +exports['onRequest'] = function(test){ + test.expect(5); + var readSession = sessions.readSession; + var s = { + session_key:'_node', + secret: 'secret', + timeout: 86400 + }; + var req = {}; + + 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'); + return 'testsession'; + }; + var next = function(){ + test.ok(true, 'chain.next called'); + test.equals( + req.session, 'testsession', 'req.session equals session data' + ); + }; + sessions(s)(req, 'res', next); + + // restore copied functions + sessions.readSession = readSession; + test.done(); +}; + +exports['writeHead'] = function(test){ + test.expect(6); + + var s = { + session_key:'_node', + secret: 'secret', + timeout: 86400 + }; + var req = {headers: {cookie: "_node="}}; + var res = { + writeHead: function(code, headers){ + test.equals( + headers['Set-Cookie'], + '_node=serialized_session; ' + + 'expires=expiry_date; ' + + 'path=/' + ); + test.equals(headers['original'], 'header'); + } + }; + + var serialize = sessions.serialize; + sessions.serialize = function(secret, data){ + test.equals(secret, 'secret', 'serialize called with secret'); + test.same(data, {test:'test'}, 'serialize called with session data'); + return 'serialized_session'; + }; + + var expires = sessions.expires; + sessions.expires = function(timeout){ + test.equals(timeout, s.timeout); + return 'expiry_date'; + }; + + var next = function(){ + test.ok(true, 'chain.next called'); + req.session = {test:'test'}; + res.writeHead(200, {'original':'header'}); + // restore copied functions + sessions.serialize = serialize; + sessions.expires = expires; + test.done(); + }; + sessions(s)(req, res, next); +}; + +exports['writeHead doesnt write cookie if none exists and session is undefined'] = function(test){ + test.expect(3); + + var s = { + session_key:'_node', + secret: 'secret', + timeout: 86400 + }; + var req = {headers: {}}; + var res = { + writeHead: function(code, headers){ + test.ok(!("Set-Cookie" in headers)); + test.equals(headers['original'], 'header'); + } + }; + + var next = function(){ + test.ok(true, 'chain.next called'); + req.session = undefined; + res.writeHead(200, {'original':'header'}); + test.done(); + }; + sessions(s)(req, res, next); +}; + +exports['writeHead writes empty cookie with immediate expiration if session is undefined'] = function(test){ + test.expect(4); + + var s = { + session_key:'_node', + secret: 'secret', + timeout: 86400 + }; + var req = {headers: {cookie: "_node=Blah"}}; + var res = { + writeHead: function(code, headers){ + test.equals( + headers['Set-Cookie'], + '_node=; ' + + 'expires=now; ' + + 'path=/' + ); + test.equals(headers['original'], 'header'); + } + }; + + var expires = sessions.expires; + sessions.expires = function(timeout){ + test.equals(timeout, 0); + return 'now'; + }; + var readSession = sessions.readSession; + sessions.readSession = function(key, secret, timeout, req) { + return {"username": "Bob"}; + }; + + var next = function(){ + test.ok(true, 'chain.next called'); + req.session = undefined; + res.writeHead(200, {'original':'header'}); + // restore copied functions + sessions.expires = expires; + sessions.readSession = readSession; + test.done(); + }; + sessions(s)(req, res, next); +}; + +exports['onInit secret set'] = function(test){ + test.expect(0); + var s = {secret: 'secret'}; + try { + sessions({secret: 'secret'}); + } + catch(e){ + test.ok(false, 'do nothing if secret set in server settings'); + } + test.done(); +}; + +exports['onInit no secret set'] = function(test){ + test.expect(1); + try { + sessions({}); + } + catch(e){ + test.ok(true, 'throw exception if no secret set in server settings'); + } + test.done(); +}; + +exports['set multiple cookies'] = 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, 12345); + return 'expiry_date'; + }; + + var req = {headers: {cookie:''}}; + var res = {writeHead: function(statusCode, headers){ + test.equals(statusCode, 200); + test.same(headers, [ + ['other_header', 'val'], + ['Set-Cookie', 'testcookie=testvalue'], + ['Set-Cookie', '_node=session_data; ' + + 'expires=expiry_date; ' + + 'path=/'] + ]); + sessions.serialize = _serialize; + sessions.expires = _expires; + test.done(); + }}; + + sessions({secret: 'secret', timeout: 12345})(req, res, function(){ + req.session = {test: 'test'}; + res.writeHead(200, { + 'other_header': 'val', + 'Set-Cookie':'testcookie=testvalue' + }); + }); +}; + +exports['set single cookie'] = 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, 12345); + return 'expiry_date'; + }; + + var req = {headers: {cookie:''}}; + 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=/' + }); + sessions.serialize = _serialize; + sessions.expires = _expires; + test.done(); + }}; + sessions({secret: 'secret', timeout: 12345})(req, res, function(){ + req.session = {test: 'test'}; + res.writeHead(200, {'other_header': 'val'}); + }); +}; + +exports['handle headers as array'] = 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, 12345); + return 'expiry_date'; + }; + + var req = {headers: {cookie:''}}; + var res = {writeHead: function(statusCode, headers){ + test.equals(statusCode, 200); + test.same(headers, [ + ['header1', 'val1'], + ['header2', 'val2'], + ['Set-Cookie', '_node=session_data; ' + + 'expires=expiry_date; ' + + 'path=/'] + ]); + sessions.serialize = _serialize; + test.done(); + }}; + sessions({secret: 'secret', timeout: 12345})(req, res, function(){ + req.session = {test: 'test'}; + res.writeHead(200, [['header1', 'val1'],['header2', 'val2']]); + }); +}; + +exports['convert headers to array'] = function(test){ + test.same( + sessions.headersToArray({'key1':'val1','key2':'val2'}), + [['key1','val1'],['key2','val2']] + ); + test.same( + sessions.headersToArray([['key1','val1'],['key2','val2']]), + [['key1','val1'],['key2','val2']] + ); + test.done(); +}; + +exports['send cookies even if there are no headers'] = function (test) { + test.expect(2); + var req = {headers: {cookie:''}}; + var res = { + writeHead: function (code, headers) { + test.equal(code, 200); + test.ok(headers['Set-Cookie']); + test.done(); + } + }; + sessions({secret: 'secret', timeout: 12345})(req, res, function () { + req.session = {test: 'test'}; + res.writeHead(200); + }); +}; + +exports['send cookies when no headers but reason_phrase'] = function (test) { + test.expect(3); + var req = {headers: {cookie:''}}; + var res = { + writeHead: function (code, reason_phrase, headers) { + test.equal(code, 200); + test.equal(reason_phrase, 'reason'); + test.ok(headers['Set-Cookie']); + test.done(); + } + }; + sessions({secret: 'secret', timeout: 12345})(req, res, function () { + req.session = {test: 'test'}; + res.writeHead(200, 'reason'); + }); +};