From 8c702cd2f6cabe96ad1900507b876c2e027f1131 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Thu, 21 Aug 2014 19:51:58 -0700 Subject: [PATCH] Move state.js out. For #1877 --- lib/index.js | 2 +- lib/response/headers.js | 4 +- lib/route.js | 48 +++- lib/state.js | 511 --------------------------------------- package.json | 1 + test/state.js | 521 ---------------------------------------- 6 files changed, 49 insertions(+), 1038 deletions(-) delete mode 100755 lib/state.js diff --git a/lib/index.js b/lib/index.js index 8058c36ac..286feb5d1 100755 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,7 @@ exports.Server = require('./server'); exports.Pack = require('./pack'); exports.state = { - prepareValue: require('./state').prepareValue + prepareValue: require('statehood').prepareValue }; exports.createServer = function () { diff --git a/lib/response/headers.js b/lib/response/headers.js index b203014ee..ab87dd563 100755 --- a/lib/response/headers.js +++ b/lib/response/headers.js @@ -2,7 +2,7 @@ var Boom = require('boom'); var Items = require('items'); -var State = require('../state'); +var Statehood = require('statehood'); var Auth = null; // Delay load due to circular dependencies @@ -224,7 +224,7 @@ internals.state = function (response, request, next) { return next(); } - State.generateSetCookieHeader(states, request.server, function (err, header) { + Statehood.format(states, request.server, function (err, header) { if (err) { return next(err); diff --git a/lib/route.js b/lib/route.js index a4b8cdab2..95a2eae75 100755 --- a/lib/route.js +++ b/lib/route.js @@ -1,11 +1,11 @@ // Load modules var Catbox = require('catbox'); -var Joi = require('joi'); var Hoek = require('hoek'); +var Joi = require('joi'); +var Statehood = require('statehood'); var Schema = require('./schema'); var Request = require('./request'); -var State = require('./state'); var Auth = require('./auth'); var Payload = require('./payload'); var Validation = require('./validation'); @@ -142,7 +142,7 @@ internals.Route.prototype.lifecycle = function () { // 'onRequest' if (this.server.settings.state.cookies.parse) { - cycle.push(State.parseCookies); + cycle.push(internals.state); } cycle.push('onPreAuth'); @@ -540,3 +540,45 @@ internals.compareMixed = function (aSegment, bSegment, p) { } } }; + + +internals.state = function (request, next) { + + request.state = {}; + + var req = request.raw.req; + var cookies = req.headers.cookie; + if (!cookies) { + return next(); + } + + var definitions = request.server._stateDefinitions; + var settings = request.server.settings.state; + + Statehood.parse(cookies, definitions, settings, function (err, state, invalids) { + + request.state = state; + + var names = Object.keys(invalids); + for (var i = 0, il = names.length; i < il; ++i) { + var name = names[i]; + var definition = definitions[name]; + + if (definition && + definition.clearInvalid !== undefined ? definition.clearInvalid : settings.cookies.clearInvalid) { + + request._clearState(name); + } + + // failAction: 'error', 'log', 'ignore' + + var failAction = (definition && definition.failAction !== undefined ? definition.failAction + : settings.cookies.failAction); + if (failAction !== 'ignore') { + request.log(['hapi', 'state', 'error'], invalids[name]); + } + } + + return next(err); + }); +}; diff --git a/lib/state.js b/lib/state.js deleted file mode 100755 index e6576b360..000000000 --- a/lib/state.js +++ /dev/null @@ -1,511 +0,0 @@ -// Load modules - -var Querystring = require('querystring'); -var Iron = require('iron'); -var Items = require('items'); -var Cryptiles = require('cryptiles'); -var Boom = require('boom'); -var Hoek = require('hoek'); - - -// Declare internals - -var internals = { - macPrefix: 'hapi.signed.cookie.1' -}; - - -// Header format - -// 1: name 2: quoted 3: value -internals.parseRx = /\s*([^=\s]+)\s*=\s*(?:(?:"([^\"]*)")|([^\;]*))(?:(?:;|(?:\s*\,)\s*)|$)/g; - -internals.validateRx = { - nameRx: { - strict: /^[^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+$/, - loose: /^[^=\s]+$/ - }, - valueRx: { - strict: /^[^\x00-\x20\"\,\;\\\x7F]*$/, - loose: /^(?:"([^\"]*)")|(?:[^\;]*)$/ - }, - domainRx: /^\.?[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d]))(?:\.[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d])))*$/, - domainLabelLenRx: /^\.?[a-z\d\-]{1,63}(?:\.[a-z\d\-]{1,63})*$/, - pathRx: /^\/[^\x00-\x1F\;]*$/ -}; - - -// Read and parse body - -exports.parseCookies = function (request, next) { - - var parse = function () { - - request.state = {}; - - var req = request.raw.req; - var cookies = req.headers.cookie; - if (!cookies) { - return next(); - } - - // Parse header - - var state = {}; - var names = []; - var verify = cookies.replace(internals.parseRx, function ($0, $1, $2, $3) { - - var name = $1; - var value = $2 || $3 || ''; - - if (state[name]) { - if (!Array.isArray(state[name])) { - state[name] = [state[name]]; - } - - state[name].push(value); - } - else { - state[name] = value; - names.push(name); - } - - return ''; - }); - - // Validate cookie header syntax - - if (verify !== '' && - shouldStop(cookies)) { - - return; // shouldStop calls next() - } - - // Parse cookies - - var parsed = {}; - Items.serial(names, function (name, nextName) { - - var value = state[name]; - var definition = request.server._stateDefinitions[name]; - - // Validate cookie - - var strict = (definition && definition.strictHeader !== undefined ? definition.strictHeader - : request.server.settings.state.cookies.strictHeader); - if (strict) { - if (!name.match(internals.validateRx.nameRx.strict)) { - if (shouldStop(cookies, name, definition)) { - return; // shouldStop calls next() - } - } - - var values = [].concat(state[name]); - for (var v = 0, vl = values.length; v < vl; ++v) { - if (!values[v].match(internals.validateRx.valueRx.strict)) { - if (shouldStop(cookies, name, definition)) { - return; // shouldStop calls next() - } - } - } - } - - // Check cookie format - - if (!definition || - !definition.encoding) { - - parsed[name] = value; - return nextName(); - } - - // Single value - - if (Array.isArray(value) === false) { - unsign(name, value, definition, function (err, unsigned) { - - if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { - return; // shouldStop calls next() - } - - return nextName(); - } - - decode(unsigned, definition, function (err, result) { - - if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { - return; // shouldStop calls next() - } - - return nextName(); - } - - parsed[name] = result; - return nextName(); - }); - }); - - return; - } - - // Array - - var arrayResult = []; - Items.serial(value, function (arrayValue, nextArray) { - - unsign(name, arrayValue, definition, function (err, unsigned) { - - if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { - return; // shouldStop calls next() - } - - return nextName(); - } - - decode(unsigned, definition, function (err, result) { - - if (err) { - if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) { - return; // shouldStop calls next() - } - - return nextName(); - } - - arrayResult.push(result); - nextArray(); - }); - }); - }, - function (err) { - - parsed[name] = arrayResult; - return nextName(); - }); - }, - function (err) { - - // All cookies parsed - - request.state = parsed; - return next(); - }); - }; - - // Extract signature and validate - - var unsign = function (name, value, definition, innerNext) { - - if (!definition.sign) { - return innerNext(null, value); - } - - var pos = value.lastIndexOf('.'); - if (pos === -1) { - return innerNext(Boom.badRequest('Missing signature separator')); - } - - var unsigned = value.slice(0, pos); - var sig = value.slice(pos + 1); - - if (!sig) { - return innerNext(Boom.badRequest('Missing signature')); - } - - var sigParts = sig.split('*'); - if (sigParts.length !== 2) { - return innerNext(Boom.badRequest('Bad signature format')); - } - - var hmacSalt = sigParts[0]; - var hmac = sigParts[1]; - - var macOptions = Hoek.clone(definition.sign.integrity || Iron.defaults.integrity); - macOptions.salt = hmacSalt; - Iron.hmacWithPassword(definition.sign.password, macOptions, [internals.macPrefix, name, unsigned].join('\n'), function (err, mac) { - - if (err) { - return innerNext(err); - } - - if (!Cryptiles.fixedTimeComparison(mac.digest, hmac)) { - return innerNext(Boom.badRequest('Bad hmac value')); - } - - return innerNext(null, unsigned); - }); - }; - - // Decode values - - var decode = function (value, definition, innerNext) { - - // Encodings: 'base64json', 'base64', 'form', 'iron', 'none' - - if (definition.encoding === 'iron') { - Iron.unseal(value, definition.password, definition.iron || Iron.defaults, function (err, unsealed) { - - if (err) { - return innerNext(err); - } - - return innerNext(null, unsealed); - }); - - return; - } - - var result = value; - - try { - switch (definition.encoding) { - case 'base64json': - var decoded = (new Buffer(value, 'base64')).toString('binary'); - result = JSON.parse(decoded); - break; - case 'base64': - result = (new Buffer(value, 'base64')).toString('binary'); - break; - case 'form': - result = Querystring.parse(value); - break; - } - } - catch (err) { - return innerNext(err); - } - - return innerNext(null, result); - }; - - var shouldStop = function (error, name, definition) { - - var clearInvalid = (definition && definition.clearInvalid !== undefined ? definition.clearInvalid - : request.server.settings.state.cookies.clearInvalid); - if (clearInvalid && name) { - request._clearState(name); - } - - // failAction: 'error', 'log', 'ignore' - - var failAction = (definition && definition.failAction !== undefined ? definition.failAction - : request.server.settings.state.cookies.failAction); - if (failAction === 'log' || - failAction === 'error') { - - request.log(['hapi', 'state', 'error'], error); - } - - if (failAction === 'error') { - next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + Hoek.escapeHtml(name) : 'header'))); - return true; - } - - return false; - }; - - parse(); -}; - - -exports.generateSetCookieHeader = function (cookies, server, callback) { - - var definitions = server._stateDefinitions || {}; - - if (!cookies || - (Array.isArray(cookies) && !cookies.length)) { - - return Hoek.nextTick(callback)(null, []); - } - - if (!Array.isArray(cookies)) { - cookies = [cookies]; - } - - var header = []; - Items.serial(cookies, function (cookie, next) { - - var options = cookie.options || {}; - - // Apply server state configuration - - if (definitions[cookie.name]) { - options = Hoek.applyToDefaults(definitions[cookie.name], options); - } - - // Validate name - - var nameRx = (server.settings.state.cookies.strictHeader ? internals.validateRx.nameRx.strict : internals.validateRx.nameRx.loose); - if (!cookie.name.match(nameRx)) { - return callback(Boom.badImplementation('Invalid cookie name: ' + cookie.name)); - } - - // Prepare value (encode, sign) - - exports.prepareValue(cookie.name, cookie.value, options, function (err, value) { - - if (err) { - return callback(err); - } - - // Validate prepared value - - var valueRx = (server.settings.state.cookies.strictHeader ? internals.validateRx.valueRx.strict : internals.validateRx.valueRx.loose); - if (value && - (typeof value !== 'string' || !value.match(valueRx))) { - - return callback(Boom.badImplementation('Invalid cookie value: ' + cookie.value)); - } - - // Construct cookie - - var segment = cookie.name + '=' + (value || ''); - - if (options.ttl !== null && - options.ttl !== undefined) { // Can be zero - - var expires = new Date(options.ttl ? Date.now() + options.ttl : 0); - segment += '; Max-Age=' + Math.floor(options.ttl / 1000) + '; Expires=' + expires.toUTCString(); - } - - if (options.isSecure) { - segment += '; Secure'; - } - - if (options.isHttpOnly) { - segment += '; HttpOnly'; - } - - if (options.domain) { - var domain = options.domain.toLowerCase(); - if (!domain.match(internals.validateRx.domainLabelLenRx)) { - return callback(Boom.badImplementation('Cookie domain too long: ' + options.domain)); - } - - if (!domain.match(internals.validateRx.domainRx)) { - return callback(Boom.badImplementation('Invalid cookie domain: ' + options.domain)); - } - - segment += '; Domain=' + domain; - } - - if (options.path) { - if (!options.path.match(internals.validateRx.pathRx)) { - return callback(Boom.badImplementation('Invalid cookie path: ' + options.path)); - } - - segment += '; Path=' + options.path; - } - - header.push(segment); - return next(); - }); - }, - function (err) { - - return callback(null, header); - }); -}; - - -exports.prepareValue = function (name, value, options, callback) { - - Hoek.assert(options && typeof options === 'object', 'Missing or invalid options'); - - // Encode value - - internals.encode(value, options, function (err, encoded) { - - if (err) { - return callback(Boom.badImplementation('Failed to encode cookie (' + name + ') value: ' + err.message)); - } - - // Sign cookie - - internals.sign(name, encoded, options.sign, function (err, signed) { - - if (err) { - return callback(Boom.badImplementation('Failed to sign cookie (' + name + ') value: ' + err.message)); - } - - return callback(null, signed); - }); - }); -}; - - -internals.encode = function (value, options, callback) { - - callback = Hoek.nextTick(callback); - - // Encodings: 'base64json', 'base64', 'form', 'iron', 'none' - - if (value === undefined) { - return callback(null, value); - } - - if (!options.encoding || - options.encoding === 'none') { - - return callback(null, value); - } - - if (options.encoding === 'iron') { - Iron.seal(value, options.password, options.iron || Iron.defaults, function (err, sealed) { - - if (err) { - return callback(err); - } - - return callback(null, sealed); - }); - - return; - } - - var result = value; - - try { - switch (options.encoding) { - case 'base64': - result = (new Buffer(value, 'binary')).toString('base64'); - break; - case 'base64json': - var stringified = JSON.stringify(value); - result = (new Buffer(stringified, 'binary')).toString('base64'); - break; - case 'form': - result = Querystring.stringify(value); - break; - } - } - catch (err) { - return callback(err); - } - - return callback(null, result); -}; - - -internals.sign = function (name, value, options, callback) { - - if (value === undefined || - !options) { - - return Hoek.nextTick(callback)(null, value); - } - - Iron.hmacWithPassword(options.password, options.integrity || Iron.defaults.integrity, [internals.macPrefix, name, value].join('\n'), function (err, mac) { - - if (err) { - return callback(err); - } - - var signed = value + '.' + mac.salt + '*' + mac.digest; - return callback(null, signed); - }); -}; - diff --git a/package.json b/package.json index 9c160a05f..aeebbe384 100755 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "kilt": "^1.1.x", "wreck": "3.x.x", "shot": "1.x.x", + "statehood": "1.x.x", "topo": "1.x.x", "qs": "1.x.x", diff --git a/test/state.js b/test/state.js index 4d3ceeeae..abd03cf21 100755 --- a/test/state.js +++ b/test/state.js @@ -5,7 +5,6 @@ var Iron = require('iron'); var Hoek = require('hoek'); var Cryptiles = require('cryptiles'); var Hapi = require('..'); -var State = require('../lib/state'); var Defaults = require('../lib/defaults'); @@ -132,524 +131,4 @@ describe('State', function () { done(); }); }); - - describe('#parseCookies', function () { - - describe('cases', function () { - - var pass = function (header, values, settings, definitions) { - - it('parses cookie header: ' + header, function (done) { - - var server = new Hapi.Server({ state: settings || Defaults.server.state }); - - if (definitions) { - var cookies = Object.keys(definitions); - for (var i = 0, il = cookies.length; i < il; ++i) { - var cookie = cookies[i]; - server.state(cookie, definitions[cookie]); - } - } - - server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.state); } }); - - server.inject({ method: 'GET', url: '/', headers: { cookie: header } }, function (res) { - - expect(res.statusCode).to.equal(200); - if (values) { - expect(res.result).to.deep.equal(values); - } - done(); - }); - }); - }; - - pass('a=b', { a: 'b' }); - pass('a=', { a: '' }); - pass('a=""', { a: '' }); - pass('a=;', { a: '' }); - pass('a=123', { a: '123' }); - pass('a=1; a=2', { a: ['1', '2'] }); - pass('a=1; b="2"; c=3', { a: '1', b: '2', c: '3' }); - pass('a="1"; b="2"; c=3;', { a: '1', b: '2', c: '3' }); - pass('A = b; b = c', { A: 'b', b: 'c' }); - pass('a="b=123456789&c=something"', { a: 'b=123456789&c=something' }); - pass('a=%1;b=x', { a: '%1', b: 'x' }); - pass('z=%20%22%2c%3b%2f', { z: '%20%22%2c%3b%2f' }); - - pass('a="b=123456789&c=something%20else"', { a: { b: '123456789', c: 'something else' } }, null, { a: { encoding: 'form' } }); - pass('a="b=%p123456789"', { a: { b: '%p123456789' } }, null, { a: { encoding: 'form' } }); - pass('a=dGVzdA; a=dGVzdA', { a: ['test', 'test'] }, null, { a: { encoding: 'base64' } }); - pass('a=dGVzdA; a=dGVzdA; a=dGVzdA', { a: ['test', 'test', 'test'] }, null, { a: { encoding: 'base64' } }); - pass('key=dGVzdA==', { key: 'test' }, null, { key: { encoding: 'base64' } }); - pass('key=dGVzdA', { key: 'test' }, null, { key: { encoding: 'base64' } }); - pass('key=dGVzdA', { key: 'dGVzdA' }, null, { key: { encoding: 'none' } }); - pass('key=eyJ0ZXN0aW5nIjoianNvbiJ9', { key: { testing: 'json' } }, null, { key: { encoding: 'base64json' } }); - pass('key=Fe26.2**f3fc42242467f7a97c042be866a32c1e7645045c2cc085124eadc66d25fc8395*URXpH8k-R0d4O5bnY23fRQ*uq9rd8ZzdjZqUrq9P2Ci0yZ-EEUikGzxTLn6QTcJ0bc**3880c0ac8bab054f529afec8660ebbbbc8050e192e39e5d622e7ac312b9860d0*r_g7N9kJYqXDrFlvOnuKpfpEWwrJLOKMXEI43LAGeFg', { key: { a: 1, b: 2, c: 3 } }, null, { key: { encoding: 'iron', password: 'password' } }); - pass('key=Fe26.2**f3fc42242467f7a97c042be866a32c1e7645045c2cc085124eadc66d25fc8395*URXpH8k-R0d4O5bnY23fRQ*uq9rd8ZzdjZqUrq9P2Ci0yZ-EEUikGzxTLn6QTcJ0bc**3880c0ac8bab054f529afec8660ebbbbc8050e192e39e5d622e7ac312b9860d0*r_g7N9kJYqXDrFlvOnuKpfpEWwrJLOKMXEI43LAGeFg', { key: { a: 1, b: 2, c: 3 } }, null, { key: { encoding: 'iron', password: 'password', iron: Iron.defaults } }); - pass('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*xGhc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo', { sid: { a: '1', b: '2', c: '3 x' } }, null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - pass('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*xGhc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo', { sid: { a: '1', b: '2', c: '3 x' } }, null, { sid: { encoding: 'form', sign: { password: 'password', integrity: Iron.defaults.integrity } } }); - pass('a="1', { a: '"1' }, null, { a: { strictHeader: false } }); - - var loose = Hoek.clone(Defaults.server.state); - loose.cookies.strictHeader = false; - pass('a="1; b="2"; c=3; d[1]=4', { a: '"1', b: '2', c: '3', 'd[1]': '4' }, loose); - - var fail = function (header, settings, definitions, result) { - - it('fails parsing cookie header: ' + header, function (done) { - - var logged = false; - var cleared = ''; - - var request = { - raw: { - req: { - headers: { - cookie: header - } - } - }, - server: { - _stateDefinitions: definitions || {}, - settings: { - state: settings || Defaults.server.state - } - }, - log: function (tags, data) { - - logged = true; - }, - _clearState: function (name) { - - cleared = name; - } - }; - - State.parseCookies(request, function (err) { - - if (request.server.settings.state.cookies.failAction !== 'error') { - expect(err).to.not.exist; - expect(settings.failAction !== 'ignore' || logged).to.equal(true); - } - else { - expect(err).to.exist; - expect(logged).to.equal(true); - } - - if (request.server.settings.state.cookies.clearInvalid) { - expect(cleared).to.equal('sid'); - } - - expect(request.state).to.deep.equal(result || {}); - done(); - }); - }); - }; - - fail('a="1; b="2"; c=3'); - fail('a@="1"; b="2"; c=3'); - fail('a=1; b=2; c=3;;'); - fail('key=XeyJ0ZXN0aW5nIjoianNvbiJ9', null, { key: { encoding: 'base64json' } }); - fail('x=XeyJ0ZXN0aW5nIjoianNvbiJ9; x=XeyJ0ZXN0aW5dnIjoianNvbiJ9', null, { x: { encoding: 'base64json' } }); - fail('key=Fe26.1**f3fc42242467f7a97c042be866a32c1e7645045c2cc085124eadc66d25fc8395*URXpH8k-R0d4O5bnY23fRQ*uq9rd8ZzdjZqUrq9P2Ci0yZ-EEUikGzxTLn6QTcJ0bc**3880c0ac8bab054f529afec8660ebbbbc8050e192e39e5d622e7ac312b9860d0*r_g7N9kJYqXDrFlvOnuKpfpEWwrJLOKMXEI43LAGeFg', null, { key: { encoding: 'iron', password: 'password' } }); - fail('key=Fe26.2**f3fc42242467f7a97c042be866a32c1e7645045c2cc085124eadc66d25fc8395*URXpH8k-R0d4O5bnY23fRQ*uq9rd8ZzdjZqUrq9P2Ci0yZ-EEUikGzxTLn6QTcJ0bc**3880c0ac8bab054f529afec8660ebbbbc8050e192e39e5d622e7ac312b9860d0*r_g7N9kJYqXDrFlvOnuKpfpEWwrJLOKMXEI43LAGeFg', null, { key: { encoding: 'iron', password: 'passwordx' } }); - fail('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*khsb8lmkNJS-iljqDKZDMmd__2PcHBz7Ksrc-48gZ-0', null, { sid: { encoding: 'form', sign: {} } }); - fail('sid=a=1&b=2&c=3%20x', null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('sid=a=1&b=2&c=3%20x; sid=a=1&b=2&c=3%20x', null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('sid=a=1&b=2&c=3%20x.', null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8', null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*-Ghc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo', null, { sid: { encoding: 'form', sign: { password: 'password' } } }); - - var setLog = Hoek.clone(Defaults.server.state); - setLog.cookies.failAction = 'log'; - fail('abc="xyzf', setLog, null, { abc: '"xyzf'}); - fail('"abc=xyzf', setLog, null, { '"abc': 'xyzf' }); - fail('key=XeyJ0ZXN0aW5nIjoianNvbiJ9', setLog, { key: { encoding: 'base64json' } }); - fail('y=XeyJ0ZXN0aW5nIjoianNvbiJ9; y=XeyJ0ZXN0aW5dnIjoianNvbiJ9', setLog, { y: { encoding: 'base64json' } }); - fail('sid=a=1&b=2&c=3%20x', setLog, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('sid=a=1&b=2&c=3%20x; sid=a=1&b=2&c=3%20x', setLog, { sid: { encoding: 'form', sign: { password: 'password' } } }); - fail('a=1; b=2; key=XeyJ0ZXN0aW5nIjoianNvbiJ9', setLog, { key: { encoding: 'base64json' } }, { a: '1', b: '2' }); - - var clearInvalid = Hoek.clone(Defaults.server.state); - clearInvalid.cookies.clearInvalid = true; - fail('sid=a=1&b=2&c=3%20x', clearInvalid, { sid: { encoding: 'form', sign: { password: 'password' } } }); - }); - - it('uses cookie failAction override', function (done) { - - var server = new Hapi.Server(); - server.state('test', { failAction: 'log' }); - server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.state.test); } }); - - server.inject({ method: 'GET', url: '/', headers: { cookie: 'test="a' } }, function (res) { - - expect(res.statusCode).to.equal(200); - expect(res.result).to.equal('"a'); - done(); - }); - }); - - it('uses cookie clearInvalid override', function (done) { - - var server = new Hapi.Server(); - server.state('test', { clearInvalid: true, failAction: 'ignore', encoding: 'base64json' }); - server.route({ method: 'GET', path: '/', handler: function (request, reply) { reply(request.state.test); } }); - - server.inject({ method: 'GET', url: '/', headers: { cookie: 'test=a' } }, function (res) { - - expect(res.statusCode).to.equal(200); - expect(res.result).to.equal(null); - expect(res.headers['set-cookie'][0]).to.equal('test=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); - done(); - }); - }); - }); - - describe('#generateSetCookieHeader', function () { - - var generateSetCookieHeader = function (cookies, definition, callback) { - - var server = new Hapi.Server(); - if (definition) { - var names = Object.keys(definition); - names.forEach(function (item) { - - server.state(item, definition[item]); - }); - } - - State.generateSetCookieHeader(cookies, server, callback); - }; - - it('skips an empty header', function (done) { - - generateSetCookieHeader(null, null, function (err, header) { - - expect(err).to.not.exist; - expect(header).to.deep.equal([]); - done(); - }); - }); - - it('skips an empty array', function (done) { - - generateSetCookieHeader([], null, function (err, header) { - - expect(err).to.not.exist; - expect(header).to.deep.equal([]); - done(); - }); - }); - - it('formats a header', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { ttl: 3600, isSecure: true, isHttpOnly: true, path: '/', domain: 'example.com' } }, null, function (err, header) { - - var expires = new Date(Date.now() + 3600); - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=fihfieuhr9384hf; Max-Age=3; Expires=' + expires.toUTCString() + '; Secure; HttpOnly; Domain=example.com; Path=/'); - done(); - }); - }); - - it('formats a header (with null ttl)', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { ttl: null, isSecure: true, isHttpOnly: true, path: '/', domain: 'example.com' } }, null, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=fihfieuhr9384hf; Secure; HttpOnly; Domain=example.com; Path=/'); - done(); - }); - }); - - it('formats a header with null value', function (done) { - - generateSetCookieHeader({ name: 'sid', options: { ttl: 3600, isSecure: true, isHttpOnly: true, path: '/', domain: 'example.com' } }, null, function (err, header) { - - var expires = new Date(Date.now() + 3600); - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=; Max-Age=3; Expires=' + expires.toUTCString() + '; Secure; HttpOnly; Domain=example.com; Path=/'); - done(); - }); - }); - - it('formats a header with server definition', function (done) { - - var definitions = { sid: { ttl: 3600, isSecure: true, isHttpOnly: true, path: '/', domain: 'example.com' } }; - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf' }, definitions, function (err, header) { - - var expires = new Date(Date.now() + 3600); - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=fihfieuhr9384hf; Max-Age=3; Expires=' + expires.toUTCString() + '; Secure; HttpOnly; Domain=example.com; Path=/'); - done(); - }); - }); - - it('formats a header with server definition (base64)', function (done) { - - var definitions = { sid: { encoding: 'base64' } }; - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf' }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=ZmloZmlldWhyOTM4NGhm'); - done(); - }); - }); - - it('formats a header with server definition (base64json)', function (done) { - - var definitions = { sid: { encoding: 'base64json' } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: 3 } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=eyJhIjoxLCJiIjoyLCJjIjozfQ=='); - done(); - }); - }); - - it('fails on a header with server definition and bad value (base64json)', function (done) { - - var definitions = { sid: { encoding: 'base64json' } }; - var bad = { a: {} }; - bad.b = bad.a; - bad.a.x = bad.b; - - generateSetCookieHeader({ name: 'sid', value: bad }, definitions, function (err, header) { - - expect(err).to.exist; - done(); - }); - }); - - it('formats a header with server definition (form)', function (done) { - - var definitions = { sid: { encoding: 'form' } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: '3 x' } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=a=1&b=2&c=3%20x'); - done(); - }); - }); - - it('formats a header with server definition (form+sign)', function (done) { - - var definitions = { - sid: { - encoding: 'form', - sign: { - password: 'password', - integrity: { - saltBits: 256, - algorithm: 'sha256', - iterations: 1, - salt: '2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8' - } - } - } - }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: '3 x' } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=a=1&b=2&c=3%20x.2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8*xGhc6WvkE55V-TzucCl0NVFmbijeCwgs5Hf5tAVbSUo'); - done(); - }); - }); - - it('formats a header with server definition (form+sign, buffer password)', function (done) { - - var buffer = new Buffer('fa4321e8c21b44a49d382fa7709226855f40eb23a32b2f642c3fd797c958718e', 'base64'); - var definitions = { - sid: { - encoding: 'form', - sign: { - password: buffer, - integrity: { - saltBits: 256, - algorithm: 'sha256', - iterations: 1, - salt: '2d75635d74c1a987f84f3ee7f3113b9a2ff71f89d6692b1089f19d5d11d140f8' - } - } - } - }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: '3 x' } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=a=1&b=2&c=3%20x.*4wjD4tIxyiNW-rC3xBqL56TxUbb_aQT5PMykruWlR0Q'); - done(); - }); - }); - - it('fails a header with bad server definition (form+sign)', function (done) { - - var definitions = { - sid: { - encoding: 'form', - sign: {} - } - }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: '3 x' } }, definitions, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Failed to sign cookie (sid) value: Empty password'); - done(); - }); - }); - - it('formats a header with server definition (iron)', function (done) { - - var definitions = { sid: { encoding: 'iron', password: 'password' } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: 3 } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.have.string('sid=Fe26.2*'); - done(); - }); - }); - - it('formats a header with server definition (iron + options)', function (done) { - - var definitions = { sid: { encoding: 'iron', password: 'password', iron: Iron.defaults } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: 3 } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.have.string('sid=Fe26.2*'); - done(); - }); - }); - - it('formats a header with server definition (iron + options, buffer password)', function (done) { - - var definitions = { sid: { encoding: 'iron', password: Cryptiles.randomBits(256), iron: Iron.defaults } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: 3 } }, definitions, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.have.string('sid=Fe26.2*'); - done(); - }); - }); - - it('fails a header with bad server definition (iron)', function (done) { - - var definitions = { sid: { encoding: 'iron' } }; - generateSetCookieHeader({ name: 'sid', value: { a: 1, b: 2, c: 3 } }, definitions, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Failed to encode cookie (sid) value: Empty password'); - done(); - }); - }); - - it('formats a header with multiple cookies', function (done) { - - generateSetCookieHeader([ - { name: 'sid', value: 'fihfieuhr9384hf', options: { ttl: 3600, isSecure: true, isHttpOnly: true, path: '/', domain: 'example.com' } }, - { name: 'pid', value: 'xyz' } - ], null, function (err, header) { - - var expires = new Date(Date.now() + 3600); - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=fihfieuhr9384hf; Max-Age=3; Expires=' + expires.toUTCString() + '; Secure; HttpOnly; Domain=example.com; Path=/'); - expect(header[1]).to.equal('pid=xyz'); - done(); - }); - }); - - it('fails on bad cookie name', function (done) { - - generateSetCookieHeader({ name: 's;id', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: 'example.com' } }, null, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Invalid cookie name: s;id'); - done(); - }); - }); - - it('allows bad cookie name in loose mode', function (done) { - - var server = { _stateDefinitions: null, settings: Hoek.clone(Defaults.server) }; - server.settings.state.cookies.strictHeader = false; - State.generateSetCookieHeader({ name: 's;id', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: 'example.com' } }, server, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('s;id=fihfieuhr9384hf; Secure; Domain=example.com; Path=/'); - done(); - }); - }); - - it('fails on bad cookie value', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fi"hfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: 'example.com' } }, null, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Invalid cookie value: fi"hfieuhr9384hf'); - done(); - }); - }); - - it('allows bad cookie value in loose mode', function (done) { - - var server = { _stateDefinitions: null, settings: Hoek.clone(Defaults.server) }; - server.settings.state.cookies.strictHeader = false; - State.generateSetCookieHeader({ name: 'sid', value: 'fi"hfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: 'example.com' } }, server, function (err, header) { - - expect(err).to.not.exist; - expect(header[0]).to.equal('sid=fi"hfieuhr9384hf; Secure; Domain=example.com; Path=/'); - done(); - }); - }); - - it('fails on bad cookie domain', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: '-example.com' } }, null, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Invalid cookie domain: -example.com'); - done(); - }); - }); - - it('fails on too long cookie domain', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: '1234567890123456789012345678901234567890123456789012345678901234567890.example.com' } }, null, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Cookie domain too long: 1234567890123456789012345678901234567890123456789012345678901234567890.example.com'); - done(); - }); - }); - - it('formats a header with cookie domain with . prefix', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: '/', domain: '.12345678901234567890.example.com' } }, null, function (err, header) { - - expect(err).to.not.exist; - done(); - }); - }); - - it('fails on bad cookie path', function (done) { - - generateSetCookieHeader({ name: 'sid', value: 'fihfieuhr9384hf', options: { isSecure: true, isHttpOnly: false, path: 'd', domain: 'example.com' } }, null, function (err, header) { - - expect(err).to.exist; - expect(err.message).to.equal('Invalid cookie path: d'); - done(); - }); - }); - }); - - describe('#prepareValue', function () { - - it('throws when missing options', function (done) { - - expect(function () { - - Hapi.state.prepareValue('name', 'value'); - }).to.throw('Missing or invalid options'); - done(); - }); - }); });