Skip to content

Commit

Permalink
Cookie specific overrides. Closes #1679
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Jun 3, 2014
1 parent b7c304d commit 48511f8
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 40 deletions.
3 changes: 3 additions & 0 deletions docs/Reference.md
Expand Up @@ -925,6 +925,9 @@ can be registered with the server using the `server.state()` method, where:
- `password` - password used for HMAC key generation.
- `password` - password used for `'iron'` encoding.
- `iron` - options for `'iron'` encoding. Defaults to [`require('iron').defaults`](https://github.com/hueniverse/iron#options).
- `failAction` - overrides the default server `state.cookies.failAction` setting.
- `clearInvalid` - overrides the default server `state.cookies.clearInvalid` setting.
- `strictHeader` - overrides the default server `state.cookies.strictHeader` setting.

```javascript
// Set cookie definition
Expand Down
9 changes: 9 additions & 0 deletions lib/defaults.js
Expand Up @@ -140,6 +140,9 @@ exports.cors = {
credentials: false
};


// Security headers

exports.security = {
hsts: 15768000,
xframe: 'deny',
Expand All @@ -160,6 +163,12 @@ exports.cache = { // Primary cache configurati

exports.state = {

// Validation settings

strictHeader: undefined, // Defaults to server.settings.state.cookies.strictHeader
failAction: undefined, // Defaults to server.settings.state.cookies.failAction
clearInvalid: undefined, // Defaults to server.settings.state.cookies.clearInvalid

// Cookie attributes

isSecure: false,
Expand Down
20 changes: 20 additions & 0 deletions lib/schema.js
Expand Up @@ -340,4 +340,24 @@ internals.register = Joi.object({
vhost: internals.vhost
}),
select: internals.labels
});


internals.state = Joi.object({
strictHeader: Joi.boolean(),
failAction: Joi.string().valid('error', 'log', 'ignore'),
clearInvalid: Joi.boolean(),
isSecure: Joi.boolean(),
isHttpOnly: Joi.boolean(),
path: Joi.string(),
domain: Joi.string(),
ttl: Joi.number(),
encoding: Joi.string().valid('base64json', 'base64', 'form', 'iron', 'none'),
sign: Joi.object({
password: Joi.string(),
integrity: Joi.object()
}),
iron: Joi.object(),
password: Joi.string(),
autoValue: Joi.any()
});
4 changes: 3 additions & 1 deletion lib/server.js
Expand Up @@ -463,7 +463,9 @@ internals.Server.prototype.state = function (name, options) {

Hoek.assert(name && typeof name === 'string', 'Invalid name');
Hoek.assert(!this._stateDefinitions[name], 'State already defined:', name);
Hoek.assert(!options || !options.encoding || ['base64json', 'base64', 'form', 'iron', 'none'].indexOf(options.encoding) !== -1, 'Bad encoding');
if (options) {
Schema.assert('state', options, name);
}

this._stateDefinitions[name] = Hoek.applyToDefaults(Defaults.state, options || {});
};
Expand Down
73 changes: 34 additions & 39 deletions lib/state.js
Expand Up @@ -39,7 +39,7 @@ internals.validateRx = {

exports.parseCookies = function (request, next) {

var prepare = function () {
var parse = function () {

request.state = {};

Expand All @@ -49,12 +49,10 @@ exports.parseCookies = function (request, next) {
return next();
}

header(cookies);
};

var header = function (cookies) {
// Parse header

var state = {};
var names = [];
var verify = cookies.replace(internals.parseRx, function ($0, $1, $2, $3) {

var name = $1;
Expand All @@ -69,6 +67,7 @@ exports.parseCookies = function (request, next) {
}
else {
state[name] = value;
names.push(name);
}

return '';
Expand All @@ -82,43 +81,37 @@ exports.parseCookies = function (request, next) {
return; // shouldStop calls next()
}

// Validate cookie
// Parse cookies

var parsed = {};
Async.forEachSeries(names, function (name, nextName) {

var value = state[name];
var definition = request.server._stateDefinitions[name];

if (request.server.settings.state.cookies.strictHeader) {
var names = Object.keys(state);
for (var i = 0, il = names.length; i < il; ++i) {
var name = names[i];
// 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)) {
if (shouldStop(cookies, name, definition)) {
return; // shouldStop calls next()
}
}

var values = [].concat(state[name]);
for (var v = 0, vl = values.length; v < vl; ++v) {
var value = values[v];
if (!value.match(internals.validateRx.valueRx.strict)) {
if (shouldStop(cookies, name)) {
if (!values[v].match(internals.validateRx.valueRx.strict)) {
if (shouldStop(cookies, name, definition)) {
return; // shouldStop calls next()
}
}
}
}
}

parse(state);
};

var parse = function (state) {

var parsed = {};

var names = Object.keys(state);
Async.forEachSeries(names, function (name, nextName) {

var value = state[name];
// Check cookie format

var definition = request.server._stateDefinitions[name];
if (!definition ||
!definition.encoding) {

Expand All @@ -132,7 +125,7 @@ exports.parseCookies = function (request, next) {
unsign(name, value, definition, function (err, unsigned) {

if (err) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) {
return; // shouldStop calls next()
}

Expand All @@ -142,7 +135,7 @@ exports.parseCookies = function (request, next) {
decode(unsigned, definition, function (err, result) {

if (err) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) {
return; // shouldStop calls next()
}

Expand All @@ -165,7 +158,7 @@ exports.parseCookies = function (request, next) {
unsign(name, arrayValue, definition, function (err, unsigned) {

if (err) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) {
return; // shouldStop calls next()
}

Expand All @@ -175,7 +168,7 @@ exports.parseCookies = function (request, next) {
decode(unsigned, definition, function (err, result) {

if (err) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name, definition)) {
return; // shouldStop calls next()
}

Expand Down Expand Up @@ -288,31 +281,33 @@ exports.parseCookies = function (request, next) {
return innerNext(null, result);
};

var shouldStop = function (error, name) {

if (request.server.settings.state.cookies.clearInvalid &&
name) {
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'

if (request.server.settings.state.cookies.failAction === 'log' ||
request.server.settings.state.cookies.failAction === 'error') {
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 (request.server.settings.state.cookies.failAction === 'error') {
if (failAction === 'error') {
next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + Hoek.escapeHtml(name) : 'header')));
return true;
}

return false;
};

prepare();
parse();
};


Expand Down
30 changes: 30 additions & 0 deletions test/state.js
Expand Up @@ -173,6 +173,7 @@ describe('State', function () {
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;
Expand Down Expand Up @@ -258,6 +259,35 @@ describe('State', function () {
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 () {
Expand Down

0 comments on commit 48511f8

Please sign in to comment.