hapi
Version:
HTTP Server framework
495 lines (344 loc) • 13.9 kB
JavaScript
// Load modules
var Querystring = require('querystring');
var Iron = require('iron');
var Async = require('async');
var Cryptiles = require('cryptiles');
var Boom = require('boom');
var Utils = require('./utils');
// Declare internals
var internals = {
macPrefix: 'hapi.signed.cookie.1'
};
// Header format
// 1: name 2: quoted value 3: value
internals.strictRx = /\s*([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)\s*=\s*(?:(?:"([^\x00-\x20\"\,\;\\\x7F]*)")|([^\x00-\x20\"\,\;\\\x7F]*))(?:(?:;|(?:\s*\,)\s*)|$)/g;
internals.looseRx = /\s*([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)\s*=\s*(?:(?:"([^\"]*)")|([^\;]*))(?:(?:;|(?:\s*\,)\s*)|$)/g;
// Read and parse body
exports.parseCookies = function (request, next) {
var prepare = function () {
if (!request.server.settings.state.cookies.parse) {
return next();
}
request.state = {};
var req = request.raw.req;
var cookies = req.headers.cookie;
if (!cookies) {
return next();
}
header(cookies);
};
var header = function (cookies) {
var state = {};
var formatRx = (request.server.settings.state.cookies.strictHeader ? internals.strictRx : internals.looseRx);
var verify = cookies.replace(formatRx, function ($0, $1, $2, $3) {
var name = $1;
var value = $2 || $3;
if (state[name]) {
if (state[name] instanceof Array === false) {
state[name] = [state[name]];
}
state[name].push(value);
}
else {
state[name] = value;
}
return '';
});
// Validate cookie header syntax
if (verify !== '' &&
shouldStop(cookies)) {
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];
var definition = request.server._stateDefinitions[name];
if (!definition ||
!definition.encoding) {
parsed[name] = value;
return nextName();
}
// Single value
if (value instanceof Array === false) {
unsign(name, value, definition, function (err, unsigned) {
if (err) {
if (shouldStop({ name: name, value: value, settings: definition, reason: err.message }, name)) {
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)) {
return; // shouldStop calls next()
}
return nextName();
}
parsed[name] = result;
return nextName();
});
});
return;
}
// Array
var arrayResult = [];
Async.forEachSeries(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)) {
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)) {
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 ||
!definition.sign) {
return innerNext(null, value);
}
var pos = value.lastIndexOf('.');
if (pos === -1) {
return innerNext(Boom.internal('Missing signature separator'));
}
var unsigned = value.slice(0, pos);
var sig = value.slice(pos + 1);
if (!sig) {
return innerNext(Boom.internal('Missing signature'));
}
sigParts = sig.split('*');
if (sigParts.length !== 2) {
return innerNext(Boom.internal('Bad signature format'));
}
var hmacSalt = sigParts[0];
var hmac = sigParts[1];
var macOptions = Utils.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.internal('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) {
if (request.server.settings.state.cookies.clearInvalid) {
request.clearState(name);
}
// failAction: 'error', 'log', 'ignore'
if (request.server.settings.state.cookies.failAction === 'log' ||
request.server.settings.state.cookies.failAction === 'error') {
request._log(['state', 'error'], error);
}
if (request.server.settings.state.cookies.failAction === 'error') {
next(Boom.badRequest('Bad cookie ' + (name ? 'value: ' + name : 'header')));
return true;
}
return false;
};
prepare();
};
internals.nameRegx = /^[^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+$/;
internals.valueRegx = /^[^\x00-\x20\"\,\;\\\x7F]*$/;
internals.domainRegx = /^[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d]))(?:\.[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d])))*$/;
internals.domainLabelLenRegx = /^[a-z\d\-]{1,63}(?:\.[a-z\d\-]{1,63})*$/;
internals.pathRegx = /^\/[^\x00-\x1F\;]*$/;
exports.generateSetCookieHeader = function (cookies, definitions, callback) {
definitions = definitions || {};
if (!cookies ||
(cookies instanceof Array && !cookies.length)) {
return Utils.nextTick(callback)(null, []);
}
if (cookies instanceof Array === false) {
cookies = [cookies];
}
var header = [];
var format = function () {
Async.forEachSeries(cookies, function (cookie, next) {
var options = cookie.options || {};
// Apply server state configuration
if (definitions[cookie.name]) {
options = Utils.applyToDefaults(definitions[cookie.name], options);
}
// Validate name
if (!cookie.name.match(internals.nameRegx)) {
return callback(Boom.internal('Invalid cookie name: ' + cookie.name));
}
// Prepare value (encode, sign)
exports.prepareValue(cookie.name, cookie.value, options, function (err, value) {
if (err) {
return callback(err);
}
// 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.domainLabelLenRegx)) {
return callback(Boom.internal('Cookie domain too long: ' + options.domain));
}
if (!domain.match(internals.domainRegx)) {
return callback(Boom.internal('Invalid cookie domain: ' + options.domain));
}
segment += '; Domain=' + domain;
}
if (options.path) {
if (!options.path.match(internals.pathRegx)) {
return callback(Boom.internal('Invalid cookie path: ' + options.path));
}
segment += '; Path=' + options.path;
}
header.push(segment);
return next();
});
},
function (err) {
return callback(null, header);
});
};
format();
};
exports.prepareValue = function (name, value, options, callback) {
// Encode value
internals.encode(value, options, function (err, encoded) {
if (err) {
return callback(Boom.internal('Failed to encode cookie (' + name + ') value: ' + err.message ));
}
// Validate value
if (encoded &&
(typeof encoded !== 'string' || !encoded.match(internals.valueRegx))) {
return callback(Boom.internal('Invalid cookie value: ' + value));
}
// Sign cookie
internals.sign(name, encoded, options.sign, function (err, signed) {
if (err) {
return callback(Boom.internal('Failed to sign cookie (' + name + ') value: ' + err.message));
}
return callback(null, signed);
});
});
};
internals.encode = function (value, options, callback) {
callback = Utils.nextTick(callback);
// Encodings: 'base64json', 'base64', 'form', 'iron', 'none'
if (value === undefined) {
return callback(null, value);
}
if (!options ||
!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 Utils.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);
});
};