@hapi/statehood
Advanced tools
Comparing version 6.1.2 to 7.0.0
464
lib/index.js
@@ -14,3 +14,5 @@ 'use strict'; | ||
const internals = {}; | ||
const internals = { | ||
macPrefix: 'hapi.signed.cookie.1' | ||
}; | ||
@@ -23,3 +25,3 @@ | ||
isHttpOnly: Joi.boolean(), | ||
isSameSite: Joi.valid('Strict', 'Lax').allow(false), | ||
isSameSite: Joi.valid('Strict', 'Lax', 'None', false), | ||
path: Joi.string().allow(null), | ||
@@ -35,2 +37,3 @@ domain: Joi.string().allow(null), | ||
password: [Joi.string(), Joi.binary(), Joi.object()], | ||
contextualize: Joi.function(), | ||
@@ -58,28 +61,2 @@ // Used by hapi | ||
exports.Definitions = internals.Definitions = function (options) { | ||
this.settings = Hoek.applyToDefaults(internals.defaults, options || {}); | ||
Joi.assert(this.settings, internals.schema, 'Invalid state definition defaults'); | ||
this.cookies = {}; | ||
this.names = []; | ||
}; | ||
internals.Definitions.prototype.add = function (name, options) { | ||
Hoek.assert(name && typeof name === 'string', 'Invalid name'); | ||
Hoek.assert(!this.cookies[name], 'State already defined:', name); | ||
const settings = Hoek.applyToDefaults(this.settings, options || {}, { nullOverride: true }); | ||
Joi.assert(settings, internals.schema, 'Invalid state definition: ' + name); | ||
this.cookies[name] = settings; | ||
this.names.push(name); | ||
}; | ||
internals.empty = new internals.Definitions(); | ||
// Header format | ||
@@ -108,125 +85,269 @@ | ||
internals.Definitions.prototype.parse = async function (cookies) { | ||
exports.Definitions = class { | ||
const state = {}; | ||
const names = []; | ||
const verify = cookies.replace(internals.parseRx, ($0, $1, $2, $3) => { | ||
constructor(options) { | ||
const name = $1; | ||
const value = $2 || $3 || ''; | ||
this.settings = Hoek.applyToDefaults(internals.defaults, options || {}); | ||
Joi.assert(this.settings, internals.schema, 'Invalid state definition defaults'); | ||
if (state[name]) { | ||
if (!Array.isArray(state[name])) { | ||
state[name] = [state[name]]; | ||
this.cookies = {}; | ||
this.names = []; | ||
} | ||
add(name, options) { | ||
Hoek.assert(name && typeof name === 'string', 'Invalid name'); | ||
Hoek.assert(!this.cookies[name], 'State already defined:', name); | ||
const settings = Hoek.applyToDefaults(this.settings, options || {}, { nullOverride: true }); | ||
Joi.assert(settings, internals.schema, 'Invalid state definition: ' + name); | ||
this.cookies[name] = settings; | ||
this.names.push(name); | ||
} | ||
async parse(cookies) { | ||
const state = {}; | ||
const names = []; | ||
const verify = cookies.replace(internals.parseRx, ($0, $1, $2, $3) => { | ||
const name = $1; | ||
const 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); | ||
} | ||
state[name].push(value); | ||
} | ||
else { | ||
state[name] = value; | ||
names.push(name); | ||
} | ||
return ''; | ||
}); | ||
return ''; | ||
}); | ||
// Validate cookie header syntax | ||
// Validate cookie header syntax | ||
const failed = []; // All errors | ||
const failed = []; // All errors | ||
if (verify !== '') { | ||
if (!this.settings.ignoreErrors) { | ||
throw Boom.badRequest('Invalid cookie header'); | ||
} | ||
if (verify !== '') { | ||
if (!this.settings.ignoreErrors) { | ||
throw Boom.badRequest('Invalid cookie header'); | ||
failed.push({ settings: this.settings, reason: `Header contains unexpected syntax: ${verify}` }); | ||
} | ||
failed.push({ settings: this.settings, reason: `Header contains unexpected syntax: ${verify}` }); | ||
} | ||
// Collect errors | ||
// Collect errors | ||
const errored = []; // Unignored errors | ||
const record = (reason, name, value, definition) => { | ||
const errored = []; // Unignored errors | ||
const record = (reason, name, value, definition) => { | ||
const details = { | ||
name, | ||
value, | ||
settings: definition, | ||
reason: typeof reason === 'string' ? reason : reason.message | ||
}; | ||
const details = { | ||
name, | ||
value, | ||
settings: definition, | ||
reason: typeof reason === 'string' ? reason : reason.message | ||
failed.push(details); | ||
if (!definition.ignoreErrors) { | ||
errored.push(details); | ||
} | ||
}; | ||
failed.push(details); | ||
if (!definition.ignoreErrors) { | ||
errored.push(details); | ||
} | ||
}; | ||
// Parse cookies | ||
// Parse cookies | ||
const parsed = {}; | ||
for (let i = 0; i < names.length; ++i) { | ||
const name = names[i]; | ||
const value = state[name]; | ||
const definition = this.cookies[name] || this.settings; | ||
const parsed = {}; | ||
for (let i = 0; i < names.length; ++i) { | ||
const name = names[i]; | ||
const value = state[name]; | ||
const definition = this.cookies[name] || this.settings; | ||
// Validate cookie | ||
// Validate cookie | ||
if (definition.strictHeader) { | ||
const reason = internals.validate(name, state); | ||
if (reason) { | ||
record(reason, name, value, definition); | ||
continue; | ||
} | ||
} | ||
if (definition.strictHeader) { | ||
const reason = internals.validate(name, state); | ||
if (reason) { | ||
record(reason, name, value, definition); | ||
// Check cookie format | ||
if (definition.encoding === 'none') { | ||
parsed[name] = value; | ||
continue; | ||
} | ||
// Single value | ||
if (!Array.isArray(value)) { | ||
try { | ||
const unsigned = await internals.unsign(name, value, definition); | ||
const result = await internals.decode(unsigned, definition); | ||
parsed[name] = result; | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
record(err, name, value, definition); | ||
} | ||
continue; | ||
} | ||
// Array | ||
const arrayResult = []; | ||
for (let j = 0; j < value.length; ++j) { | ||
const arrayValue = value[j]; | ||
try { | ||
const unsigned = await internals.unsign(name, arrayValue, definition); | ||
const result = await internals.decode(unsigned, definition); | ||
arrayResult.push(result); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
record(err, name, value, definition); | ||
} | ||
} | ||
parsed[name] = arrayResult; | ||
} | ||
// Check cookie format | ||
if (errored.length) { | ||
const error = Boom.badRequest('Invalid cookie value', errored); | ||
error.states = parsed; | ||
error.failed = failed; | ||
throw error; | ||
} | ||
if (definition.encoding === 'none') { | ||
parsed[name] = value; | ||
continue; | ||
return { states: parsed, failed }; | ||
} | ||
async format(cookies, context) { | ||
if (!cookies || | ||
Array.isArray(cookies) && !cookies.length) { | ||
return []; | ||
} | ||
// Single value | ||
if (!Array.isArray(cookies)) { | ||
cookies = [cookies]; | ||
} | ||
if (!Array.isArray(value)) { | ||
try { | ||
const unsigned = await internals.unsign(name, value, definition); | ||
const result = await internals.decode(unsigned, definition); | ||
parsed[name] = result; | ||
const header = []; | ||
for (let i = 0; i < cookies.length; ++i) { | ||
const cookie = cookies[i]; | ||
// Apply definition to local configuration | ||
const base = this.cookies[cookie.name] || this.settings; | ||
let definition = cookie.options ? Hoek.applyToDefaults(base, cookie.options, { nullOverride: true }) : base; | ||
// Contextualize definition | ||
if (definition.contextualize) { | ||
if (definition === base) { | ||
definition = Hoek.clone(definition); | ||
} | ||
await definition.contextualize(definition, context); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
record(err, name, value, definition); | ||
// Validate name | ||
const nameRx = definition.strictHeader ? internals.validateRx.nameRx.strict : internals.validateRx.nameRx.loose; | ||
if (!nameRx.test(cookie.name)) { | ||
throw Boom.badImplementation('Invalid cookie name: ' + cookie.name); | ||
} | ||
continue; | ||
} | ||
// Prepare value (encode, sign) | ||
// Array | ||
const value = await exports.prepareValue(cookie.name, cookie.value, definition); | ||
const arrayResult = []; | ||
for (let j = 0; j < value.length; ++j) { | ||
const arrayValue = value[j]; | ||
// Validate prepared value | ||
try { | ||
const unsigned = await internals.unsign(name, arrayValue, definition); | ||
const result = await internals.decode(unsigned, definition); | ||
arrayResult.push(result); | ||
const valueRx = definition.strictHeader ? internals.validateRx.valueRx.strict : internals.validateRx.valueRx.loose; | ||
if (value && | ||
(typeof value !== 'string' || !value.match(valueRx))) { | ||
throw Boom.badImplementation('Invalid cookie value: ' + cookie.value); | ||
} | ||
catch (err) { | ||
Bounce.rethrow(err, 'system'); | ||
record(err, name, value, definition); | ||
// Construct cookie | ||
let segment = cookie.name + '=' + (value || ''); | ||
if (definition.ttl !== null && | ||
definition.ttl !== undefined) { // Can be zero | ||
const expires = new Date(definition.ttl ? Date.now() + definition.ttl : 0); | ||
segment = segment + '; Max-Age=' + Math.floor(definition.ttl / 1000) + '; Expires=' + expires.toUTCString(); | ||
} | ||
if (definition.isSecure) { | ||
segment = segment + '; Secure'; | ||
} | ||
if (definition.isHttpOnly) { | ||
segment = segment + '; HttpOnly'; | ||
} | ||
if (definition.isSameSite) { | ||
segment = `${segment}; SameSite=${definition.isSameSite}`; | ||
} | ||
if (definition.domain) { | ||
const domain = definition.domain.toLowerCase(); | ||
if (!domain.match(internals.validateRx.domainLabelLenRx)) { | ||
throw Boom.badImplementation('Cookie domain too long: ' + definition.domain); | ||
} | ||
if (!domain.match(internals.validateRx.domainRx)) { | ||
throw Boom.badImplementation('Invalid cookie domain: ' + definition.domain); | ||
} | ||
segment = segment + '; Domain=' + domain; | ||
} | ||
if (definition.path) { | ||
if (!definition.path.match(internals.validateRx.pathRx)) { | ||
throw Boom.badImplementation('Invalid cookie path: ' + definition.path); | ||
} | ||
segment = segment + '; Path=' + definition.path; | ||
} | ||
header.push(segment); | ||
} | ||
parsed[name] = arrayResult; | ||
return header; | ||
} | ||
if (errored.length) { | ||
const error = Boom.badRequest('Invalid cookie value', errored); | ||
error.states = parsed; | ||
error.failed = failed; | ||
throw error; | ||
passThrough(header, fallback) { | ||
if (!this.names.length) { | ||
return header; | ||
} | ||
const exclude = []; | ||
for (let i = 0; i < this.names.length; ++i) { | ||
const name = this.names[i]; | ||
const definition = this.cookies[name]; | ||
const passCookie = definition.passThrough !== undefined ? definition.passThrough : fallback; | ||
if (!passCookie) { | ||
exclude.push(name); | ||
} | ||
} | ||
return exports.exclude(header, exclude); | ||
} | ||
return { states: parsed, failed }; | ||
}; | ||
@@ -252,5 +373,2 @@ | ||
internals.macPrefix = 'hapi.signed.cookie.1'; | ||
internals.unsign = async function (name, value, definition) { | ||
@@ -310,3 +428,3 @@ | ||
if (definition.encoding === 'base64json') { | ||
const decoded = (Buffer.from(value, 'base64')).toString('binary'); | ||
const decoded = Buffer.from(value, 'base64').toString('binary'); | ||
try { | ||
@@ -321,3 +439,3 @@ return Bourne.parse(decoded); | ||
if (definition.encoding === 'base64') { | ||
return (Buffer.from(value, 'base64')).toString('binary'); | ||
return Buffer.from(value, 'base64').toString('binary'); | ||
} | ||
@@ -331,94 +449,2 @@ | ||
internals.Definitions.prototype.format = async function (cookies) { | ||
if (!cookies || | ||
(Array.isArray(cookies) && !cookies.length)) { | ||
return []; | ||
} | ||
if (!Array.isArray(cookies)) { | ||
cookies = [cookies]; | ||
} | ||
const header = []; | ||
for (let i = 0; i < cookies.length; ++i) { | ||
const cookie = cookies[i]; | ||
// Apply definition to local configuration | ||
const base = this.cookies[cookie.name] || this.settings; | ||
const definition = cookie.options ? Hoek.applyToDefaults(base, cookie.options, { nullOverride: true }) : base; | ||
// Validate name | ||
const nameRx = (definition.strictHeader ? internals.validateRx.nameRx.strict : internals.validateRx.nameRx.loose); | ||
if (!nameRx.test(cookie.name)) { | ||
throw Boom.badImplementation('Invalid cookie name: ' + cookie.name); | ||
} | ||
// Prepare value (encode, sign) | ||
const value = await exports.prepareValue(cookie.name, cookie.value, definition); | ||
// Validate prepared value | ||
const valueRx = (definition.strictHeader ? internals.validateRx.valueRx.strict : internals.validateRx.valueRx.loose); | ||
if (value && | ||
(typeof value !== 'string' || !value.match(valueRx))) { | ||
throw Boom.badImplementation('Invalid cookie value: ' + cookie.value); | ||
} | ||
// Construct cookie | ||
let segment = cookie.name + '=' + (value || ''); | ||
if (definition.ttl !== null && | ||
definition.ttl !== undefined) { // Can be zero | ||
const expires = new Date(definition.ttl ? Date.now() + definition.ttl : 0); | ||
segment = segment + '; Max-Age=' + Math.floor(definition.ttl / 1000) + '; Expires=' + expires.toUTCString(); | ||
} | ||
if (definition.isSecure) { | ||
segment = segment + '; Secure'; | ||
} | ||
if (definition.isHttpOnly) { | ||
segment = segment + '; HttpOnly'; | ||
} | ||
if (definition.isSameSite) { | ||
segment = segment + `; SameSite=${definition.isSameSite}`; | ||
} | ||
if (definition.domain) { | ||
const domain = definition.domain.toLowerCase(); | ||
if (!domain.match(internals.validateRx.domainLabelLenRx)) { | ||
throw Boom.badImplementation('Cookie domain too long: ' + definition.domain); | ||
} | ||
if (!domain.match(internals.validateRx.domainRx)) { | ||
throw Boom.badImplementation('Invalid cookie domain: ' + definition.domain); | ||
} | ||
segment = segment + '; Domain=' + domain; | ||
} | ||
if (definition.path) { | ||
if (!definition.path.match(internals.validateRx.pathRx)) { | ||
throw Boom.badImplementation('Invalid cookie path: ' + definition.path); | ||
} | ||
segment = segment + '; Path=' + definition.path; | ||
} | ||
header.push(segment); | ||
} | ||
return header; | ||
}; | ||
exports.prepareValue = async function (name, value, options) { | ||
@@ -454,3 +480,3 @@ | ||
if (options.encoding === 'base64') { | ||
return (Buffer.from(value, 'binary')).toString('base64'); | ||
return Buffer.from(value, 'binary').toString('base64'); | ||
} | ||
@@ -460,3 +486,3 @@ | ||
const stringified = JSON.stringify(value); | ||
return (Buffer.from(stringified, 'binary')).toString('base64'); | ||
return Buffer.from(stringified, 'binary').toString('base64'); | ||
} | ||
@@ -484,22 +510,2 @@ | ||
internals.Definitions.prototype.passThrough = function (header, fallback) { | ||
if (!this.names.length) { | ||
return header; | ||
} | ||
const exclude = []; | ||
for (let i = 0; i < this.names.length; ++i) { | ||
const name = this.names[i]; | ||
const definition = this.cookies[name]; | ||
const passCookie = definition.passThrough !== undefined ? definition.passThrough : fallback; | ||
if (!passCookie) { | ||
exclude.push(name); | ||
} | ||
} | ||
return exports.exclude(header, exclude); | ||
}; | ||
exports.exclude = function (cookies, excludes) { | ||
@@ -506,0 +512,0 @@ |
{ | ||
"name": "@hapi/statehood", | ||
"description": "HTTP State Management Utilities", | ||
"version": "6.1.2", | ||
"version": "7.0.0", | ||
"repository": "git://github.com/hapijs/statehood", | ||
@@ -6,0 +6,0 @@ "main": "lib/index.js", |
<a href="http://hapijs.com"><img src="https://raw.githubusercontent.com/hapijs/assets/master/images/family.png" width="180px" align="right" /></a> | ||
# statehood | ||
# @hapi/statehood | ||
@@ -5,0 +5,0 @@ HTTP State Management Utilities. |
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
17804
373