From a03c3969c476f74ec393fd1b6c298c6392be2646 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Mon, 22 Nov 2021 14:15:54 +0100 Subject: [PATCH 1/4] remove cookie package, integrate cookie package integrate cookie tests rename cookie.test.js to plugin.test.js --- cookie.js | 195 +++++++++++ package.json | 1 - plugin.js | 2 +- test/cookie.test.js | 784 +++++++++----------------------------------- test/plugin.test.js | 669 +++++++++++++++++++++++++++++++++++++ 5 files changed, 1025 insertions(+), 626 deletions(-) create mode 100644 cookie.js create mode 100644 test/plugin.test.js diff --git a/cookie.js b/cookie.js new file mode 100644 index 0000000..0fd3cd1 --- /dev/null +++ b/cookie.js @@ -0,0 +1,195 @@ +/*! + * cookie + * Copyright(c) 2012-2014 Roman Shtylman + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +exports.parse = parse +exports.serialize = serialize + +/** + * Module variables. + * @private + */ + +const decode = decodeURIComponent +const encode = encodeURIComponent +const pairSplitRegExp = /; */ + +/** + * RegExp to match field-content in RFC 7230 sec 3.2 + * + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * obs-text = %x80-FF + */ + +const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line + +/** + * Parse a cookie header. + * + * Parse the given cookie header string into an object + * The object has the various cookies as keys(names) => values + * + * @param {string} str + * @param {object} [options] + * @return {object} + * @public + */ + +function parse (str, options) { + if (typeof str !== 'string') { + throw new TypeError('argument str must be a string') + } + + const obj = {} + const opt = options || {} + const pairs = str.split(pairSplitRegExp) + const dec = opt.decode || decode + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i] + let eqIdx = pair.indexOf('=') + // skip things that don't look like key=value + if (eqIdx < 0) { + continue + } + + const key = pair.substr(0, eqIdx).trim() + let val = pair.substr(++eqIdx, pair.length).trim() + // quoted values + if (val[0] === '"') { + val = val.slice(1, -1) + } + + // only assign once + if (undefined === obj[key]) { + obj[key] = tryDecode(val, dec) + } + } + + return obj +} + +/** + * Serialize data into a cookie header. + * + * Serialize the a name value pair into a cookie string suitable for + * http headers. An optional options object specified cookie parameters. + * + * serialize('foo', 'bar', { httpOnly: true }) + * => "foo=bar; httpOnly" + * + * @param {string} name + * @param {string} val + * @param {object} [options] + * @return {string} + * @public + */ + +function serialize (name, val, options) { + const opt = options || {} + const enc = opt.encode || encode + if (typeof enc !== 'function') { + throw new TypeError('option encode is invalid') + } + + if (!fieldContentRegExp.test(name)) { + throw new TypeError('argument name is invalid') + } + + const value = enc(val) + if (value && !fieldContentRegExp.test(value)) { + throw new TypeError('argument val is invalid') + } + + let str = name + '=' + value + if (opt.maxAge != null) { + const maxAge = opt.maxAge - 0 + if (isNaN(maxAge) || !isFinite(maxAge)) { + throw new TypeError('option maxAge is invalid') + } + + str += '; Max-Age=' + Math.floor(maxAge) + } + + if (opt.domain) { + if (!fieldContentRegExp.test(opt.domain)) { + throw new TypeError('option domain is invalid') + } + + str += '; Domain=' + opt.domain + } + + if (opt.path) { + if (!fieldContentRegExp.test(opt.path)) { + throw new TypeError('option path is invalid') + } + + str += '; Path=' + opt.path + } + + if (opt.expires) { + if (typeof opt.expires.toUTCString !== 'function') { + throw new TypeError('option expires is invalid') + } + + str += '; Expires=' + opt.expires.toUTCString() + } + + if (opt.httpOnly) { + str += '; HttpOnly' + } + + if (opt.secure) { + str += '; Secure' + } + + if (opt.sameSite) { + const sameSite = typeof opt.sameSite === 'string' + ? opt.sameSite.toLowerCase() + : opt.sameSite + switch (sameSite) { + case true: + str += '; SameSite=Strict' + break + case 'lax': + str += '; SameSite=Lax' + break + case 'strict': + str += '; SameSite=Strict' + break + case 'none': + str += '; SameSite=None' + break + default: + throw new TypeError('option sameSite is invalid') + } + } + + return str +} + +/** + * Try decoding a string using a decoding function. + * + * @param {string} str + * @param {function} decode + * @private + */ + +function tryDecode (str, decode) { + try { + return decode(str) + } catch (e) { + return str + } +} diff --git a/package.json b/package.json index d6cf78a..b9ee409 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "typescript": "^4.4.4" }, "dependencies": { - "cookie": "^0.4.1", "cookie-signature": "^1.1.0", "fastify-plugin": "^3.0.0" }, diff --git a/plugin.js b/plugin.js index ff15ed0..b1c1917 100644 --- a/plugin.js +++ b/plugin.js @@ -1,7 +1,7 @@ 'use strict' const fp = require('fastify-plugin') -const cookie = require('cookie') +const cookie = require('./cookie') const signerFactory = require('./signer') diff --git a/test/cookie.test.js b/test/cookie.test.js index 47209cd..e8e10f7 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -2,668 +2,204 @@ const tap = require('tap') const test = tap.test -const Fastify = require('fastify') -const sinon = require('sinon') -const cookieSignature = require('cookie-signature') -const plugin = require('../') -test('cookies get set correctly', (t) => { - t.plan(7) - const fastify = Fastify() - fastify.register(plugin) +const cookie = require('../cookie') - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { path: '/' }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - }) +test('parse: argument validation', (t) => { + t.plan(2) + t.throws(cookie.parse.bind(), /argument str must be a string/) + t.throws(cookie.parse.bind(null, 42), /argument str must be a string/) + t.end() }) -test('express cookie compatibility', (t) => { - t.plan(7) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/espresso', (req, reply) => { - reply - .cookie('foo', 'foo', { path: '/' }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/espresso' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - }) +test('parse: basic', (t) => { + t.plan(2) + t.same(cookie.parse('foo=bar'), { foo: 'bar' }) + t.same(cookie.parse('foo=123'), { foo: '123' }) + t.end() }) -test('should set multiple cookies', (t) => { - t.plan(10) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/', (req, reply) => { - reply - .setCookie('foo', 'foo') - .cookie('bar', 'test') - .setCookie('wee', 'woo') - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 3) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[1].name, 'bar') - t.equal(cookies[1].value, 'test') - t.equal(cookies[2].name, 'wee') - t.equal(cookies[2].value, 'woo') - }) +test('parse: ignore spaces', (t) => { + t.plan(1) + t.same(cookie.parse('FOO = bar; baz = raz'), { FOO: 'bar', baz: 'raz' }) + t.end() }) -test('cookies get set correctly with millisecond dates', (t) => { - t.plan(8) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { path: '/', expires: Date.now() + 1000 }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - const expires = new Date(cookies[0].expires) - t.ok(expires < new Date(Date.now() + 5000)) - }) +test('parse: escaping', (t) => { + t.plan(2) + t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"'), { foo: 'bar=123456789&name=Magic+Mouse' }) + t.same(cookie.parse('email=%20%22%2c%3b%2f'), { email: ' ",;/' }) + t.end() }) -test('share options for setCookie and clearCookie', (t) => { - t.plan(11) - const fastify = Fastify() - const secret = 'testsecret' - fastify.register(plugin, { secret }) - - const cookieOptions = { - signed: true, - maxAge: 36000 - } - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .clearCookie('foo', cookieOptions) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 2) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) - t.equal(cookies[0].maxAge, 36000) - - t.equal(cookies[1].name, 'foo') - t.equal(cookies[1].value, '') - t.equal(cookies[1].path, '/') - t.ok(new Date(cookies[1].expires) < new Date()) - }) +test('parse: ignore escaping error and return original value', (t) => { + t.plan(1) + t.same(cookie.parse('foo=%1;bar=bar'), { foo: '%1', bar: 'bar' }) + t.end() }) -test('expires should not be overridden in clearCookie', (t) => { - t.plan(11) - const fastify = Fastify() - const secret = 'testsecret' - fastify.register(plugin, { secret }) - - const cookieOptions = { - signed: true, - expires: Date.now() + 1000 - } - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .clearCookie('foo', cookieOptions) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 2) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) - const expires = new Date(cookies[0].expires) - t.ok(expires < new Date(Date.now() + 5000)) - - t.equal(cookies[1].name, 'foo') - t.equal(cookies[1].value, '') - t.equal(cookies[1].path, '/') - t.equal(Number(cookies[1].expires), 0) - }) +test('parse: ignore non values', (t) => { + t.plan(1) + t.same(cookie.parse('foo=%1;bar=bar;HttpOnly;Secure'), + { foo: '%1', bar: 'bar' }) + t.end() }) -test('parses incoming cookies', (t) => { - t.plan(6 + 3 * 3) - const fastify = Fastify() - fastify.register(plugin) - - // check that it parses the cookies in the onRequest hook - for (const hook of ['preValidation', 'preHandler']) { - fastify.addHook(hook, (req, reply, done) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - done() - }) - } - - fastify.addHook('preParsing', (req, reply, payload, done) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - done() - }) +test('parse: unencoded', (t) => { + t.plan(2) + t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"', { + decode: function (v) { return v } + }), { foo: 'bar=123456789&name=Magic+Mouse' }) + + t.same(cookie.parse('email=%20%22%2c%3b%2f', { + decode: function (v) { return v } + }), { email: '%20%22%2c%3b%2f' }) + t.end() +}) - fastify.get('/test2', (req, reply) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - reply.send({ hello: 'world' }) - }) +test('parse: dates', (t) => { + t.plan(1) + t.same(cookie.parse('priority=true; expires=Wed, 29 Jan 2014 17:43:25 GMT; Path=/', { + decode: function (v) { return v } + }), { priority: 'true', Path: '/', expires: 'Wed, 29 Jan 2014 17:43:25 GMT' }) + t.end() +}) - fastify.inject({ - method: 'GET', - url: '/test2', - headers: { - cookie: 'bar=bar' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - }) +test('parse: missing value', (t) => { + t.plan(1) + t.same(cookie.parse('foo; bar=1; fizz= ; buzz=2', { + decode: function (v) { return v } + }), { bar: '1', fizz: '', buzz: '2' }) + t.end() }) -test('does not modify supplied cookie options object', (t) => { +test('parse: assign only once', (t) => { t.plan(3) - const expireDate = Date.now() + 1000 - const cookieOptions = { - path: '/', - expires: expireDate - } - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .send({ hello: 'world' }) - }) + t.same(cookie.parse('foo=%1;bar=bar;foo=boo'), { foo: '%1', bar: 'bar' }) + t.same(cookie.parse('foo=false;bar=bar;foo=true'), { foo: 'false', bar: 'bar' }) + t.same(cookie.parse('foo=;bar=bar;foo=boo'), { foo: '', bar: 'bar' }) + t.end() +}) - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.strictSame(cookieOptions, { - path: '/', - expires: expireDate - }) - }) +test('serializer: basic', (t) => { + t.plan(6) + t.same(cookie.serialize('foo', 'bar'), 'foo=bar') + t.same(cookie.serialize('foo', 'bar baz'), 'foo=bar%20baz') + t.same(cookie.serialize('foo', ''), 'foo=') + t.throws(cookie.serialize.bind(cookie, 'foo\n', 'bar'), /argument name is invalid/) + t.throws(cookie.serialize.bind(cookie, 'foo\u280a', 'bar'), /argument name is invalid/) + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { encode: 42 }), /option encode is invalid/) + t.end() }) -test('cookies gets cleared correctly', (t) => { - t.plan(5) - const fastify = Fastify() - fastify.register(plugin) +test('serializer: path', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { path: '/' }), 'foo=bar; Path=/') + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + path: '/\n' + }), /option path is invalid/) + t.end() +}) - fastify.get('/test1', (req, reply) => { - reply - .clearCookie('foo') - .send({ hello: 'world' }) - }) +test('serializer: secure', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { secure: true }), 'foo=bar; Secure') + t.same(cookie.serialize('foo', 'bar', { secure: false }), 'foo=bar') + t.end() +}) - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) +test('serializer: domain', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { domain: 'example.com' }), 'foo=bar; Domain=example.com') + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + domain: 'example.com\n' + }), /option domain is invalid/) + t.end() +}) - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(new Date(cookies[0].expires) < new Date(), true) - }) +test('serializer: httpOnly', (t) => { + t.plan(1) + t.same(cookie.serialize('foo', 'bar', { httpOnly: true }), 'foo=bar; HttpOnly') + t.end() }) -test('cookies signature', (t) => { +test('serializer: maxAge', (t) => { t.plan(9) - - t.test('unsign', t => { - t.plan(6) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookieSignature.unsign(cookies[0].value, secret), 'foo') - }) - }) - - t.test('key rotation uses first key to sign', t => { - t.plan(6) - const fastify = Fastify() - const secret1 = 'secret-1' - const secret2 = 'secret-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'cookieVal', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookieSignature.unsign(cookies[0].value, secret1), 'cookieVal') // decode using first key - }) - }) - - t.test('unsginCookie via fastify instance', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, rep) => { - rep.send({ - unsigned: fastify.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via reply decorator', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator after rotation', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret2)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) - }) - }) - - t.test('unsignCookie via reply decorator after rotation', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret2)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator failure response', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) - }) - }) - - t.test('unsignCookie reply decorator failure response', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) - }) - }) + t.throws(function () { + cookie.serialize('foo', 'bar', { + maxAge: 'buzz' + }) + }, /option maxAge is invalid/) + + t.throws(function () { + cookie.serialize('foo', 'bar', { + maxAge: Infinity + }) + }, /option maxAge is invalid/) + + t.same(cookie.serialize('foo', 'bar', { maxAge: 1000 }), 'foo=bar; Max-Age=1000') + t.same(cookie.serialize('foo', 'bar', { maxAge: '1000' }), 'foo=bar; Max-Age=1000') + t.same(cookie.serialize('foo', 'bar', { maxAge: 0 }), 'foo=bar; Max-Age=0') + t.same(cookie.serialize('foo', 'bar', { maxAge: '0' }), 'foo=bar; Max-Age=0') + t.same(cookie.serialize('foo', 'bar', { maxAge: null }), 'foo=bar') + t.same(cookie.serialize('foo', 'bar', { maxAge: undefined }), 'foo=bar') + t.same(cookie.serialize('foo', 'bar', { maxAge: 3.14 }), 'foo=bar; Max-Age=3') + t.end() }) -test('custom signer', t => { - t.plan(7) - const fastify = Fastify() - const signStub = sinon.stub().returns('SIGNED-VALUE') - const unsignStub = sinon.stub().returns('ORIGINAL VALUE') - const secret = { sign: signStub, unsign: unsignStub } - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'bar', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'SIGNED-VALUE') - t.ok(signStub.calledOnceWithExactly('bar')) - }) +test('serializer: expires', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { + expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)) + }), 'foo=bar; Expires=Sun, 24 Dec 2000 10:30:59 GMT') + + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + expires: Date.now() + }), /option expires is invalid/) + t.end() }) -test('unsignCookie decorator with custom signer', t => { - t.plan(4) - const fastify = Fastify() - const signStub = sinon.stub().returns('SIGNED-VALUE') - const unsignStub = sinon.stub().returns('ORIGINAL VALUE') - const secret = { sign: signStub, unsign: unsignStub } - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: 'foo=SOME-SIGNED-VALUE' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: 'ORIGINAL VALUE' }) - t.ok(unsignStub.calledOnceWithExactly('SOME-SIGNED-VALUE')) - }) +test('sameSite', (t) => { + t.plan(9) + t.same(cookie.serialize('foo', 'bar', { sameSite: true }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'Strict' }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'strict' }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'Lax' }), 'foo=bar; SameSite=Lax') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'lax' }), 'foo=bar; SameSite=Lax') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'None' }), 'foo=bar; SameSite=None') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'none' }), 'foo=bar; SameSite=None') + t.same(cookie.serialize('foo', 'bar', { sameSite: false }), 'foo=bar') + + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + sameSite: 'foo' + }), /option sameSite is invalid/) + t.end() }) -test('pass options to `cookies.parse`', (t) => { - t.plan(6) - const fastify = Fastify() - fastify.register(plugin, { - parseOptions: { - decode: decoder - } - }) - - fastify.get('/test1', (req, reply) => { - t.ok(req.cookies) - t.ok(req.cookies.foo) - t.equal(req.cookies.foo, 'bartest') - reply.send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: 'foo=bar' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - }) - - function decoder (str) { - return str + 'test' - } +test('escaping', (t) => { + t.plan(1) + t.same(cookie.serialize('cat', '+ '), 'cat=%2B%20') + t.end() }) -test('issue 53', (t) => { - t.plan(5) - const fastify = Fastify() - fastify.register(plugin) - - let cookies - let count = 1 - fastify.get('/foo', (req, reply) => { - if (count > 1) { - t.not(cookies, req.cookies) - return reply.send('done') - } - - count += 1 - cookies = req.cookies - reply.send('done') - }) - - fastify.inject({ url: '/foo' }, (err, response) => { - t.error(err) - t.equal(response.body, 'done') - }) +test('parse->serialize', (t) => { + t.plan(2) + t.same(cookie.parse(cookie.serialize('cat', 'foo=123&name=baz five')), + { cat: 'foo=123&name=baz five' }) - fastify.inject({ url: '/foo' }, (err, response) => { - t.error(err) - t.equal(response.body, 'done') - }) + t.same(cookie.parse(cookie.serialize('cat', ' ";/')), + { cat: ' ";/' }) + t.end() }) -test('parse cookie manually using decorator', (t) => { +test('unencoded', (t) => { t.plan(2) - const fastify = Fastify() - fastify.register(plugin) - - fastify.ready(() => { - t.ok(fastify.parseCookie) - t.same(fastify.parseCookie('foo=bar', {}), { foo: 'bar' }) - t.end() - }) + t.same(cookie.serialize('cat', '+ ', { + encode: function (value) { return value } + }), 'cat=+ ') + + t.throws(cookie.serialize.bind(cookie, 'cat', '+ \n', { + encode: function (value) { return value } + }), /argument val is invalid/) + t.end() }) diff --git a/test/plugin.test.js b/test/plugin.test.js new file mode 100644 index 0000000..47209cd --- /dev/null +++ b/test/plugin.test.js @@ -0,0 +1,669 @@ +'use strict' + +const tap = require('tap') +const test = tap.test +const Fastify = require('fastify') +const sinon = require('sinon') +const cookieSignature = require('cookie-signature') +const plugin = require('../') + +test('cookies get set correctly', (t) => { + t.plan(7) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { path: '/' }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + }) +}) + +test('express cookie compatibility', (t) => { + t.plan(7) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/espresso', (req, reply) => { + reply + .cookie('foo', 'foo', { path: '/' }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/espresso' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + }) +}) + +test('should set multiple cookies', (t) => { + t.plan(10) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/', (req, reply) => { + reply + .setCookie('foo', 'foo') + .cookie('bar', 'test') + .setCookie('wee', 'woo') + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 3) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[1].name, 'bar') + t.equal(cookies[1].value, 'test') + t.equal(cookies[2].name, 'wee') + t.equal(cookies[2].value, 'woo') + }) +}) + +test('cookies get set correctly with millisecond dates', (t) => { + t.plan(8) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { path: '/', expires: Date.now() + 1000 }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + const expires = new Date(cookies[0].expires) + t.ok(expires < new Date(Date.now() + 5000)) + }) +}) + +test('share options for setCookie and clearCookie', (t) => { + t.plan(11) + const fastify = Fastify() + const secret = 'testsecret' + fastify.register(plugin, { secret }) + + const cookieOptions = { + signed: true, + maxAge: 36000 + } + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .clearCookie('foo', cookieOptions) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 2) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) + t.equal(cookies[0].maxAge, 36000) + + t.equal(cookies[1].name, 'foo') + t.equal(cookies[1].value, '') + t.equal(cookies[1].path, '/') + t.ok(new Date(cookies[1].expires) < new Date()) + }) +}) + +test('expires should not be overridden in clearCookie', (t) => { + t.plan(11) + const fastify = Fastify() + const secret = 'testsecret' + fastify.register(plugin, { secret }) + + const cookieOptions = { + signed: true, + expires: Date.now() + 1000 + } + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .clearCookie('foo', cookieOptions) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 2) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) + const expires = new Date(cookies[0].expires) + t.ok(expires < new Date(Date.now() + 5000)) + + t.equal(cookies[1].name, 'foo') + t.equal(cookies[1].value, '') + t.equal(cookies[1].path, '/') + t.equal(Number(cookies[1].expires), 0) + }) +}) + +test('parses incoming cookies', (t) => { + t.plan(6 + 3 * 3) + const fastify = Fastify() + fastify.register(plugin) + + // check that it parses the cookies in the onRequest hook + for (const hook of ['preValidation', 'preHandler']) { + fastify.addHook(hook, (req, reply, done) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + done() + }) + } + + fastify.addHook('preParsing', (req, reply, payload, done) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + done() + }) + + fastify.get('/test2', (req, reply) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test2', + headers: { + cookie: 'bar=bar' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + }) +}) + +test('does not modify supplied cookie options object', (t) => { + t.plan(3) + const expireDate = Date.now() + 1000 + const cookieOptions = { + path: '/', + expires: expireDate + } + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.strictSame(cookieOptions, { + path: '/', + expires: expireDate + }) + }) +}) + +test('cookies gets cleared correctly', (t) => { + t.plan(5) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test1', (req, reply) => { + reply + .clearCookie('foo') + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(new Date(cookies[0].expires) < new Date(), true) + }) +}) + +test('cookies signature', (t) => { + t.plan(9) + + t.test('unsign', t => { + t.plan(6) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookieSignature.unsign(cookies[0].value, secret), 'foo') + }) + }) + + t.test('key rotation uses first key to sign', t => { + t.plan(6) + const fastify = Fastify() + const secret1 = 'secret-1' + const secret2 = 'secret-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'cookieVal', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookieSignature.unsign(cookies[0].value, secret1), 'cookieVal') // decode using first key + }) + }) + + t.test('unsginCookie via fastify instance', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, rep) => { + rep.send({ + unsigned: fastify.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via reply decorator', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator after rotation', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret2)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) + }) + }) + + t.test('unsignCookie via reply decorator after rotation', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret2)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator failure response', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) + }) + }) + + t.test('unsignCookie reply decorator failure response', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) + }) + }) +}) + +test('custom signer', t => { + t.plan(7) + const fastify = Fastify() + const signStub = sinon.stub().returns('SIGNED-VALUE') + const unsignStub = sinon.stub().returns('ORIGINAL VALUE') + const secret = { sign: signStub, unsign: unsignStub } + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'bar', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'SIGNED-VALUE') + t.ok(signStub.calledOnceWithExactly('bar')) + }) +}) + +test('unsignCookie decorator with custom signer', t => { + t.plan(4) + const fastify = Fastify() + const signStub = sinon.stub().returns('SIGNED-VALUE') + const unsignStub = sinon.stub().returns('ORIGINAL VALUE') + const secret = { sign: signStub, unsign: unsignStub } + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: 'foo=SOME-SIGNED-VALUE' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: 'ORIGINAL VALUE' }) + t.ok(unsignStub.calledOnceWithExactly('SOME-SIGNED-VALUE')) + }) +}) + +test('pass options to `cookies.parse`', (t) => { + t.plan(6) + const fastify = Fastify() + fastify.register(plugin, { + parseOptions: { + decode: decoder + } + }) + + fastify.get('/test1', (req, reply) => { + t.ok(req.cookies) + t.ok(req.cookies.foo) + t.equal(req.cookies.foo, 'bartest') + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: 'foo=bar' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + }) + + function decoder (str) { + return str + 'test' + } +}) + +test('issue 53', (t) => { + t.plan(5) + const fastify = Fastify() + fastify.register(plugin) + + let cookies + let count = 1 + fastify.get('/foo', (req, reply) => { + if (count > 1) { + t.not(cookies, req.cookies) + return reply.send('done') + } + + count += 1 + cookies = req.cookies + reply.send('done') + }) + + fastify.inject({ url: '/foo' }, (err, response) => { + t.error(err) + t.equal(response.body, 'done') + }) + + fastify.inject({ url: '/foo' }, (err, response) => { + t.error(err) + t.equal(response.body, 'done') + }) +}) + +test('parse cookie manually using decorator', (t) => { + t.plan(2) + const fastify = Fastify() + fastify.register(plugin) + + fastify.ready(() => { + t.ok(fastify.parseCookie) + t.same(fastify.parseCookie('foo=bar', {}), { foo: 'bar' }) + t.end() + }) +}) From c3c8d798577dc2bf2bcd2b094c5788460b6c5cdd Mon Sep 17 00:00:00 2001 From: uzlopak Date: Mon, 22 Nov 2021 14:22:14 +0100 Subject: [PATCH 2/4] implement optimized cookie parse method --- cookie.js | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/cookie.js b/cookie.js index 0fd3cd1..c749892 100644 --- a/cookie.js +++ b/cookie.js @@ -22,7 +22,6 @@ exports.serialize = serialize const decode = decodeURIComponent const encode = encodeURIComponent -const pairSplitRegExp = /; */ /** * RegExp to match field-content in RFC 7230 sec 3.2 @@ -51,32 +50,42 @@ function parse (str, options) { throw new TypeError('argument str must be a string') } - const obj = {} - const opt = options || {} - const pairs = str.split(pairSplitRegExp) - const dec = opt.decode || decode - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i] - let eqIdx = pair.indexOf('=') + const result = {} + const dec = (options && options.decode) || decode + + let pos = 0 + let terminatorPos = 0 + let eqIdx = 0 + + while (true) { + if (terminatorPos === str.length) { + break + } + terminatorPos = str.indexOf(';', pos) + terminatorPos = (terminatorPos === -1) ? str.length : terminatorPos + eqIdx = str.indexOf('=', pos) + // skip things that don't look like key=value - if (eqIdx < 0) { + if (eqIdx === -1 || eqIdx > terminatorPos) { + pos = terminatorPos + 1 continue } - const key = pair.substr(0, eqIdx).trim() - let val = pair.substr(++eqIdx, pair.length).trim() - // quoted values - if (val[0] === '"') { - val = val.slice(1, -1) - } + const key = str.substring(pos, eqIdx++).trim() // only assign once - if (undefined === obj[key]) { - obj[key] = tryDecode(val, dec) + if (undefined === result[key]) { + const val = (str.charCodeAt(eqIdx) === 0x22) + ? str.substring(eqIdx + 1, terminatorPos - 1).trim() + : str.substring(eqIdx, terminatorPos).trim() + + result[key] = (dec !== decode || val.indexOf('%') !== -1) + ? tryDecode(val, dec) + : val } + pos = terminatorPos + 1 } - - return obj + return result } /** From d7d0819253c26eff23f4d9efec046662aa04f25e Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 23 Nov 2021 17:14:15 +0100 Subject: [PATCH 3/4] add License header --- cookie.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/cookie.js b/cookie.js index c749892..349db26 100644 --- a/cookie.js +++ b/cookie.js @@ -1,8 +1,29 @@ /*! - * cookie - * Copyright(c) 2012-2014 Roman Shtylman - * Copyright(c) 2015 Douglas Christopher Wilson - * MIT Licensed + * Adapted from https://github.com/jshttp/cookie + * + * (The MIT License) + * + * Copyright (c) 2012-2014 Roman Shtylman + * Copyright (c) 2015 Douglas Christopher Wilson + * + * 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. */ 'use strict' From b696cb0b1668c507a2fa7765c92bf15e91e47b74 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 23 Nov 2021 18:23:41 +0100 Subject: [PATCH 4/4] rename files --- test/cookie-module.test.js | 205 ++++++++++ test/cookie.test.js | 784 +++++++++++++++++++++++++++++-------- test/plugin.test.js | 669 ------------------------------- 3 files changed, 829 insertions(+), 829 deletions(-) create mode 100644 test/cookie-module.test.js delete mode 100644 test/plugin.test.js diff --git a/test/cookie-module.test.js b/test/cookie-module.test.js new file mode 100644 index 0000000..e8e10f7 --- /dev/null +++ b/test/cookie-module.test.js @@ -0,0 +1,205 @@ +'use strict' + +const tap = require('tap') +const test = tap.test + +const cookie = require('../cookie') + +test('parse: argument validation', (t) => { + t.plan(2) + t.throws(cookie.parse.bind(), /argument str must be a string/) + t.throws(cookie.parse.bind(null, 42), /argument str must be a string/) + t.end() +}) + +test('parse: basic', (t) => { + t.plan(2) + t.same(cookie.parse('foo=bar'), { foo: 'bar' }) + t.same(cookie.parse('foo=123'), { foo: '123' }) + t.end() +}) + +test('parse: ignore spaces', (t) => { + t.plan(1) + t.same(cookie.parse('FOO = bar; baz = raz'), { FOO: 'bar', baz: 'raz' }) + t.end() +}) + +test('parse: escaping', (t) => { + t.plan(2) + t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"'), { foo: 'bar=123456789&name=Magic+Mouse' }) + t.same(cookie.parse('email=%20%22%2c%3b%2f'), { email: ' ",;/' }) + t.end() +}) + +test('parse: ignore escaping error and return original value', (t) => { + t.plan(1) + t.same(cookie.parse('foo=%1;bar=bar'), { foo: '%1', bar: 'bar' }) + t.end() +}) + +test('parse: ignore non values', (t) => { + t.plan(1) + t.same(cookie.parse('foo=%1;bar=bar;HttpOnly;Secure'), + { foo: '%1', bar: 'bar' }) + t.end() +}) + +test('parse: unencoded', (t) => { + t.plan(2) + t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"', { + decode: function (v) { return v } + }), { foo: 'bar=123456789&name=Magic+Mouse' }) + + t.same(cookie.parse('email=%20%22%2c%3b%2f', { + decode: function (v) { return v } + }), { email: '%20%22%2c%3b%2f' }) + t.end() +}) + +test('parse: dates', (t) => { + t.plan(1) + t.same(cookie.parse('priority=true; expires=Wed, 29 Jan 2014 17:43:25 GMT; Path=/', { + decode: function (v) { return v } + }), { priority: 'true', Path: '/', expires: 'Wed, 29 Jan 2014 17:43:25 GMT' }) + t.end() +}) + +test('parse: missing value', (t) => { + t.plan(1) + t.same(cookie.parse('foo; bar=1; fizz= ; buzz=2', { + decode: function (v) { return v } + }), { bar: '1', fizz: '', buzz: '2' }) + t.end() +}) + +test('parse: assign only once', (t) => { + t.plan(3) + t.same(cookie.parse('foo=%1;bar=bar;foo=boo'), { foo: '%1', bar: 'bar' }) + t.same(cookie.parse('foo=false;bar=bar;foo=true'), { foo: 'false', bar: 'bar' }) + t.same(cookie.parse('foo=;bar=bar;foo=boo'), { foo: '', bar: 'bar' }) + t.end() +}) + +test('serializer: basic', (t) => { + t.plan(6) + t.same(cookie.serialize('foo', 'bar'), 'foo=bar') + t.same(cookie.serialize('foo', 'bar baz'), 'foo=bar%20baz') + t.same(cookie.serialize('foo', ''), 'foo=') + t.throws(cookie.serialize.bind(cookie, 'foo\n', 'bar'), /argument name is invalid/) + t.throws(cookie.serialize.bind(cookie, 'foo\u280a', 'bar'), /argument name is invalid/) + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { encode: 42 }), /option encode is invalid/) + t.end() +}) + +test('serializer: path', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { path: '/' }), 'foo=bar; Path=/') + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + path: '/\n' + }), /option path is invalid/) + t.end() +}) + +test('serializer: secure', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { secure: true }), 'foo=bar; Secure') + t.same(cookie.serialize('foo', 'bar', { secure: false }), 'foo=bar') + t.end() +}) + +test('serializer: domain', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { domain: 'example.com' }), 'foo=bar; Domain=example.com') + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + domain: 'example.com\n' + }), /option domain is invalid/) + t.end() +}) + +test('serializer: httpOnly', (t) => { + t.plan(1) + t.same(cookie.serialize('foo', 'bar', { httpOnly: true }), 'foo=bar; HttpOnly') + t.end() +}) + +test('serializer: maxAge', (t) => { + t.plan(9) + t.throws(function () { + cookie.serialize('foo', 'bar', { + maxAge: 'buzz' + }) + }, /option maxAge is invalid/) + + t.throws(function () { + cookie.serialize('foo', 'bar', { + maxAge: Infinity + }) + }, /option maxAge is invalid/) + + t.same(cookie.serialize('foo', 'bar', { maxAge: 1000 }), 'foo=bar; Max-Age=1000') + t.same(cookie.serialize('foo', 'bar', { maxAge: '1000' }), 'foo=bar; Max-Age=1000') + t.same(cookie.serialize('foo', 'bar', { maxAge: 0 }), 'foo=bar; Max-Age=0') + t.same(cookie.serialize('foo', 'bar', { maxAge: '0' }), 'foo=bar; Max-Age=0') + t.same(cookie.serialize('foo', 'bar', { maxAge: null }), 'foo=bar') + t.same(cookie.serialize('foo', 'bar', { maxAge: undefined }), 'foo=bar') + t.same(cookie.serialize('foo', 'bar', { maxAge: 3.14 }), 'foo=bar; Max-Age=3') + t.end() +}) + +test('serializer: expires', (t) => { + t.plan(2) + t.same(cookie.serialize('foo', 'bar', { + expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)) + }), 'foo=bar; Expires=Sun, 24 Dec 2000 10:30:59 GMT') + + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + expires: Date.now() + }), /option expires is invalid/) + t.end() +}) + +test('sameSite', (t) => { + t.plan(9) + t.same(cookie.serialize('foo', 'bar', { sameSite: true }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'Strict' }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'strict' }), 'foo=bar; SameSite=Strict') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'Lax' }), 'foo=bar; SameSite=Lax') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'lax' }), 'foo=bar; SameSite=Lax') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'None' }), 'foo=bar; SameSite=None') + t.same(cookie.serialize('foo', 'bar', { sameSite: 'none' }), 'foo=bar; SameSite=None') + t.same(cookie.serialize('foo', 'bar', { sameSite: false }), 'foo=bar') + + t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { + sameSite: 'foo' + }), /option sameSite is invalid/) + t.end() +}) + +test('escaping', (t) => { + t.plan(1) + t.same(cookie.serialize('cat', '+ '), 'cat=%2B%20') + t.end() +}) + +test('parse->serialize', (t) => { + t.plan(2) + t.same(cookie.parse(cookie.serialize('cat', 'foo=123&name=baz five')), + { cat: 'foo=123&name=baz five' }) + + t.same(cookie.parse(cookie.serialize('cat', ' ";/')), + { cat: ' ";/' }) + t.end() +}) + +test('unencoded', (t) => { + t.plan(2) + t.same(cookie.serialize('cat', '+ ', { + encode: function (value) { return value } + }), 'cat=+ ') + + t.throws(cookie.serialize.bind(cookie, 'cat', '+ \n', { + encode: function (value) { return value } + }), /argument val is invalid/) + t.end() +}) diff --git a/test/cookie.test.js b/test/cookie.test.js index e8e10f7..47209cd 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -2,204 +2,668 @@ const tap = require('tap') const test = tap.test +const Fastify = require('fastify') +const sinon = require('sinon') +const cookieSignature = require('cookie-signature') +const plugin = require('../') -const cookie = require('../cookie') +test('cookies get set correctly', (t) => { + t.plan(7) + const fastify = Fastify() + fastify.register(plugin) -test('parse: argument validation', (t) => { - t.plan(2) - t.throws(cookie.parse.bind(), /argument str must be a string/) - t.throws(cookie.parse.bind(null, 42), /argument str must be a string/) - t.end() -}) + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { path: '/' }) + .send({ hello: 'world' }) + }) -test('parse: basic', (t) => { - t.plan(2) - t.same(cookie.parse('foo=bar'), { foo: 'bar' }) - t.same(cookie.parse('foo=123'), { foo: '123' }) - t.end() -}) + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) -test('parse: ignore spaces', (t) => { - t.plan(1) - t.same(cookie.parse('FOO = bar; baz = raz'), { FOO: 'bar', baz: 'raz' }) - t.end() + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + }) }) -test('parse: escaping', (t) => { - t.plan(2) - t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"'), { foo: 'bar=123456789&name=Magic+Mouse' }) - t.same(cookie.parse('email=%20%22%2c%3b%2f'), { email: ' ",;/' }) - t.end() +test('express cookie compatibility', (t) => { + t.plan(7) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/espresso', (req, reply) => { + reply + .cookie('foo', 'foo', { path: '/' }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/espresso' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + }) }) -test('parse: ignore escaping error and return original value', (t) => { - t.plan(1) - t.same(cookie.parse('foo=%1;bar=bar'), { foo: '%1', bar: 'bar' }) - t.end() +test('should set multiple cookies', (t) => { + t.plan(10) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/', (req, reply) => { + reply + .setCookie('foo', 'foo') + .cookie('bar', 'test') + .setCookie('wee', 'woo') + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 3) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[1].name, 'bar') + t.equal(cookies[1].value, 'test') + t.equal(cookies[2].name, 'wee') + t.equal(cookies[2].value, 'woo') + }) }) -test('parse: ignore non values', (t) => { - t.plan(1) - t.same(cookie.parse('foo=%1;bar=bar;HttpOnly;Secure'), - { foo: '%1', bar: 'bar' }) - t.end() +test('cookies get set correctly with millisecond dates', (t) => { + t.plan(8) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { path: '/', expires: Date.now() + 1000 }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[0].path, '/') + const expires = new Date(cookies[0].expires) + t.ok(expires < new Date(Date.now() + 5000)) + }) }) -test('parse: unencoded', (t) => { - t.plan(2) - t.same(cookie.parse('foo="bar=123456789&name=Magic+Mouse"', { - decode: function (v) { return v } - }), { foo: 'bar=123456789&name=Magic+Mouse' }) - - t.same(cookie.parse('email=%20%22%2c%3b%2f', { - decode: function (v) { return v } - }), { email: '%20%22%2c%3b%2f' }) - t.end() +test('share options for setCookie and clearCookie', (t) => { + t.plan(11) + const fastify = Fastify() + const secret = 'testsecret' + fastify.register(plugin, { secret }) + + const cookieOptions = { + signed: true, + maxAge: 36000 + } + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .clearCookie('foo', cookieOptions) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 2) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) + t.equal(cookies[0].maxAge, 36000) + + t.equal(cookies[1].name, 'foo') + t.equal(cookies[1].value, '') + t.equal(cookies[1].path, '/') + t.ok(new Date(cookies[1].expires) < new Date()) + }) }) -test('parse: dates', (t) => { - t.plan(1) - t.same(cookie.parse('priority=true; expires=Wed, 29 Jan 2014 17:43:25 GMT; Path=/', { - decode: function (v) { return v } - }), { priority: 'true', Path: '/', expires: 'Wed, 29 Jan 2014 17:43:25 GMT' }) - t.end() +test('expires should not be overridden in clearCookie', (t) => { + t.plan(11) + const fastify = Fastify() + const secret = 'testsecret' + fastify.register(plugin, { secret }) + + const cookieOptions = { + signed: true, + expires: Date.now() + 1000 + } + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .clearCookie('foo', cookieOptions) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 2) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) + const expires = new Date(cookies[0].expires) + t.ok(expires < new Date(Date.now() + 5000)) + + t.equal(cookies[1].name, 'foo') + t.equal(cookies[1].value, '') + t.equal(cookies[1].path, '/') + t.equal(Number(cookies[1].expires), 0) + }) }) -test('parse: missing value', (t) => { - t.plan(1) - t.same(cookie.parse('foo; bar=1; fizz= ; buzz=2', { - decode: function (v) { return v } - }), { bar: '1', fizz: '', buzz: '2' }) - t.end() +test('parses incoming cookies', (t) => { + t.plan(6 + 3 * 3) + const fastify = Fastify() + fastify.register(plugin) + + // check that it parses the cookies in the onRequest hook + for (const hook of ['preValidation', 'preHandler']) { + fastify.addHook(hook, (req, reply, done) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + done() + }) + } + + fastify.addHook('preParsing', (req, reply, payload, done) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + done() + }) + + fastify.get('/test2', (req, reply) => { + t.ok(req.cookies) + t.ok(req.cookies.bar) + t.equal(req.cookies.bar, 'bar') + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test2', + headers: { + cookie: 'bar=bar' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + }) }) -test('parse: assign only once', (t) => { +test('does not modify supplied cookie options object', (t) => { t.plan(3) - t.same(cookie.parse('foo=%1;bar=bar;foo=boo'), { foo: '%1', bar: 'bar' }) - t.same(cookie.parse('foo=false;bar=bar;foo=true'), { foo: 'false', bar: 'bar' }) - t.same(cookie.parse('foo=;bar=bar;foo=boo'), { foo: '', bar: 'bar' }) - t.end() -}) + const expireDate = Date.now() + 1000 + const cookieOptions = { + path: '/', + expires: expireDate + } + const fastify = Fastify() + fastify.register(plugin) -test('serializer: basic', (t) => { - t.plan(6) - t.same(cookie.serialize('foo', 'bar'), 'foo=bar') - t.same(cookie.serialize('foo', 'bar baz'), 'foo=bar%20baz') - t.same(cookie.serialize('foo', ''), 'foo=') - t.throws(cookie.serialize.bind(cookie, 'foo\n', 'bar'), /argument name is invalid/) - t.throws(cookie.serialize.bind(cookie, 'foo\u280a', 'bar'), /argument name is invalid/) - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { encode: 42 }), /option encode is invalid/) - t.end() -}) + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', cookieOptions) + .send({ hello: 'world' }) + }) -test('serializer: path', (t) => { - t.plan(2) - t.same(cookie.serialize('foo', 'bar', { path: '/' }), 'foo=bar; Path=/') - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - path: '/\n' - }), /option path is invalid/) - t.end() + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.strictSame(cookieOptions, { + path: '/', + expires: expireDate + }) + }) }) -test('serializer: secure', (t) => { - t.plan(2) - t.same(cookie.serialize('foo', 'bar', { secure: true }), 'foo=bar; Secure') - t.same(cookie.serialize('foo', 'bar', { secure: false }), 'foo=bar') - t.end() -}) +test('cookies gets cleared correctly', (t) => { + t.plan(5) + const fastify = Fastify() + fastify.register(plugin) -test('serializer: domain', (t) => { - t.plan(2) - t.same(cookie.serialize('foo', 'bar', { domain: 'example.com' }), 'foo=bar; Domain=example.com') - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - domain: 'example.com\n' - }), /option domain is invalid/) - t.end() -}) + fastify.get('/test1', (req, reply) => { + reply + .clearCookie('foo') + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) -test('serializer: httpOnly', (t) => { - t.plan(1) - t.same(cookie.serialize('foo', 'bar', { httpOnly: true }), 'foo=bar; HttpOnly') - t.end() + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(new Date(cookies[0].expires) < new Date(), true) + }) }) -test('serializer: maxAge', (t) => { +test('cookies signature', (t) => { t.plan(9) - t.throws(function () { - cookie.serialize('foo', 'bar', { - maxAge: 'buzz' - }) - }, /option maxAge is invalid/) - - t.throws(function () { - cookie.serialize('foo', 'bar', { - maxAge: Infinity - }) - }, /option maxAge is invalid/) - - t.same(cookie.serialize('foo', 'bar', { maxAge: 1000 }), 'foo=bar; Max-Age=1000') - t.same(cookie.serialize('foo', 'bar', { maxAge: '1000' }), 'foo=bar; Max-Age=1000') - t.same(cookie.serialize('foo', 'bar', { maxAge: 0 }), 'foo=bar; Max-Age=0') - t.same(cookie.serialize('foo', 'bar', { maxAge: '0' }), 'foo=bar; Max-Age=0') - t.same(cookie.serialize('foo', 'bar', { maxAge: null }), 'foo=bar') - t.same(cookie.serialize('foo', 'bar', { maxAge: undefined }), 'foo=bar') - t.same(cookie.serialize('foo', 'bar', { maxAge: 3.14 }), 'foo=bar; Max-Age=3') - t.end() + + t.test('unsign', t => { + t.plan(6) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'foo', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookieSignature.unsign(cookies[0].value, secret), 'foo') + }) + }) + + t.test('key rotation uses first key to sign', t => { + t.plan(6) + const fastify = Fastify() + const secret1 = 'secret-1' + const secret2 = 'secret-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'cookieVal', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookieSignature.unsign(cookies[0].value, secret1), 'cookieVal') // decode using first key + }) + }) + + t.test('unsginCookie via fastify instance', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, rep) => { + rep.send({ + unsigned: fastify.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via reply decorator', t => { + t.plan(3) + const fastify = Fastify() + const secret = 'bar' + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator after rotation', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret2)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) + }) + }) + + t.test('unsignCookie via reply decorator after rotation', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', secret2)}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) + }) + }) + + t.test('unsignCookie via request decorator failure response', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: req.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) + }) + }) + + t.test('unsignCookie reply decorator failure response', t => { + t.plan(3) + const fastify = Fastify() + const secret1 = 'sec-1' + const secret2 = 'sec-2' + fastify.register(plugin, { secret: [secret1, secret2] }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) + }) + }) }) -test('serializer: expires', (t) => { - t.plan(2) - t.same(cookie.serialize('foo', 'bar', { - expires: new Date(Date.UTC(2000, 11, 24, 10, 30, 59, 900)) - }), 'foo=bar; Expires=Sun, 24 Dec 2000 10:30:59 GMT') - - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - expires: Date.now() - }), /option expires is invalid/) - t.end() +test('custom signer', t => { + t.plan(7) + const fastify = Fastify() + const signStub = sinon.stub().returns('SIGNED-VALUE') + const unsignStub = sinon.stub().returns('ORIGINAL VALUE') + const secret = { sign: signStub, unsign: unsignStub } + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply + .setCookie('foo', 'bar', { signed: true }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 1) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'SIGNED-VALUE') + t.ok(signStub.calledOnceWithExactly('bar')) + }) }) -test('sameSite', (t) => { - t.plan(9) - t.same(cookie.serialize('foo', 'bar', { sameSite: true }), 'foo=bar; SameSite=Strict') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'Strict' }), 'foo=bar; SameSite=Strict') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'strict' }), 'foo=bar; SameSite=Strict') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'Lax' }), 'foo=bar; SameSite=Lax') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'lax' }), 'foo=bar; SameSite=Lax') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'None' }), 'foo=bar; SameSite=None') - t.same(cookie.serialize('foo', 'bar', { sameSite: 'none' }), 'foo=bar; SameSite=None') - t.same(cookie.serialize('foo', 'bar', { sameSite: false }), 'foo=bar') - - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - sameSite: 'foo' - }), /option sameSite is invalid/) - t.end() +test('unsignCookie decorator with custom signer', t => { + t.plan(4) + const fastify = Fastify() + const signStub = sinon.stub().returns('SIGNED-VALUE') + const unsignStub = sinon.stub().returns('ORIGINAL VALUE') + const secret = { sign: signStub, unsign: unsignStub } + fastify.register(plugin, { secret }) + + fastify.get('/test1', (req, reply) => { + reply.send({ + unsigned: reply.unsignCookie(req.cookies.foo) + }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: 'foo=SOME-SIGNED-VALUE' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { unsigned: 'ORIGINAL VALUE' }) + t.ok(unsignStub.calledOnceWithExactly('SOME-SIGNED-VALUE')) + }) }) -test('escaping', (t) => { - t.plan(1) - t.same(cookie.serialize('cat', '+ '), 'cat=%2B%20') - t.end() +test('pass options to `cookies.parse`', (t) => { + t.plan(6) + const fastify = Fastify() + fastify.register(plugin, { + parseOptions: { + decode: decoder + } + }) + + fastify.get('/test1', (req, reply) => { + t.ok(req.cookies) + t.ok(req.cookies.foo) + t.equal(req.cookies.foo, 'bartest') + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1', + headers: { + cookie: 'foo=bar' + } + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + }) + + function decoder (str) { + return str + 'test' + } }) -test('parse->serialize', (t) => { - t.plan(2) - t.same(cookie.parse(cookie.serialize('cat', 'foo=123&name=baz five')), - { cat: 'foo=123&name=baz five' }) +test('issue 53', (t) => { + t.plan(5) + const fastify = Fastify() + fastify.register(plugin) + + let cookies + let count = 1 + fastify.get('/foo', (req, reply) => { + if (count > 1) { + t.not(cookies, req.cookies) + return reply.send('done') + } - t.same(cookie.parse(cookie.serialize('cat', ' ";/')), - { cat: ' ";/' }) - t.end() + count += 1 + cookies = req.cookies + reply.send('done') + }) + + fastify.inject({ url: '/foo' }, (err, response) => { + t.error(err) + t.equal(response.body, 'done') + }) + + fastify.inject({ url: '/foo' }, (err, response) => { + t.error(err) + t.equal(response.body, 'done') + }) }) -test('unencoded', (t) => { +test('parse cookie manually using decorator', (t) => { t.plan(2) - t.same(cookie.serialize('cat', '+ ', { - encode: function (value) { return value } - }), 'cat=+ ') - - t.throws(cookie.serialize.bind(cookie, 'cat', '+ \n', { - encode: function (value) { return value } - }), /argument val is invalid/) - t.end() + const fastify = Fastify() + fastify.register(plugin) + + fastify.ready(() => { + t.ok(fastify.parseCookie) + t.same(fastify.parseCookie('foo=bar', {}), { foo: 'bar' }) + t.end() + }) }) diff --git a/test/plugin.test.js b/test/plugin.test.js deleted file mode 100644 index 47209cd..0000000 --- a/test/plugin.test.js +++ /dev/null @@ -1,669 +0,0 @@ -'use strict' - -const tap = require('tap') -const test = tap.test -const Fastify = require('fastify') -const sinon = require('sinon') -const cookieSignature = require('cookie-signature') -const plugin = require('../') - -test('cookies get set correctly', (t) => { - t.plan(7) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { path: '/' }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - }) -}) - -test('express cookie compatibility', (t) => { - t.plan(7) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/espresso', (req, reply) => { - reply - .cookie('foo', 'foo', { path: '/' }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/espresso' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - }) -}) - -test('should set multiple cookies', (t) => { - t.plan(10) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/', (req, reply) => { - reply - .setCookie('foo', 'foo') - .cookie('bar', 'test') - .setCookie('wee', 'woo') - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 3) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[1].name, 'bar') - t.equal(cookies[1].value, 'test') - t.equal(cookies[2].name, 'wee') - t.equal(cookies[2].value, 'woo') - }) -}) - -test('cookies get set correctly with millisecond dates', (t) => { - t.plan(8) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { path: '/', expires: Date.now() + 1000 }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'foo') - t.equal(cookies[0].path, '/') - const expires = new Date(cookies[0].expires) - t.ok(expires < new Date(Date.now() + 5000)) - }) -}) - -test('share options for setCookie and clearCookie', (t) => { - t.plan(11) - const fastify = Fastify() - const secret = 'testsecret' - fastify.register(plugin, { secret }) - - const cookieOptions = { - signed: true, - maxAge: 36000 - } - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .clearCookie('foo', cookieOptions) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 2) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) - t.equal(cookies[0].maxAge, 36000) - - t.equal(cookies[1].name, 'foo') - t.equal(cookies[1].value, '') - t.equal(cookies[1].path, '/') - t.ok(new Date(cookies[1].expires) < new Date()) - }) -}) - -test('expires should not be overridden in clearCookie', (t) => { - t.plan(11) - const fastify = Fastify() - const secret = 'testsecret' - fastify.register(plugin, { secret }) - - const cookieOptions = { - signed: true, - expires: Date.now() + 1000 - } - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .clearCookie('foo', cookieOptions) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 2) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, cookieSignature.sign('foo', secret)) - const expires = new Date(cookies[0].expires) - t.ok(expires < new Date(Date.now() + 5000)) - - t.equal(cookies[1].name, 'foo') - t.equal(cookies[1].value, '') - t.equal(cookies[1].path, '/') - t.equal(Number(cookies[1].expires), 0) - }) -}) - -test('parses incoming cookies', (t) => { - t.plan(6 + 3 * 3) - const fastify = Fastify() - fastify.register(plugin) - - // check that it parses the cookies in the onRequest hook - for (const hook of ['preValidation', 'preHandler']) { - fastify.addHook(hook, (req, reply, done) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - done() - }) - } - - fastify.addHook('preParsing', (req, reply, payload, done) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - done() - }) - - fastify.get('/test2', (req, reply) => { - t.ok(req.cookies) - t.ok(req.cookies.bar) - t.equal(req.cookies.bar, 'bar') - reply.send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test2', - headers: { - cookie: 'bar=bar' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - }) -}) - -test('does not modify supplied cookie options object', (t) => { - t.plan(3) - const expireDate = Date.now() + 1000 - const cookieOptions = { - path: '/', - expires: expireDate - } - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', cookieOptions) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.strictSame(cookieOptions, { - path: '/', - expires: expireDate - }) - }) -}) - -test('cookies gets cleared correctly', (t) => { - t.plan(5) - const fastify = Fastify() - fastify.register(plugin) - - fastify.get('/test1', (req, reply) => { - reply - .clearCookie('foo') - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(new Date(cookies[0].expires) < new Date(), true) - }) -}) - -test('cookies signature', (t) => { - t.plan(9) - - t.test('unsign', t => { - t.plan(6) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'foo', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookieSignature.unsign(cookies[0].value, secret), 'foo') - }) - }) - - t.test('key rotation uses first key to sign', t => { - t.plan(6) - const fastify = Fastify() - const secret1 = 'secret-1' - const secret2 = 'secret-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'cookieVal', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookieSignature.unsign(cookies[0].value, secret1), 'cookieVal') // decode using first key - }) - }) - - t.test('unsginCookie via fastify instance', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, rep) => { - rep.send({ - unsigned: fastify.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via reply decorator', t => { - t.plan(3) - const fastify = Fastify() - const secret = 'bar' - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: false, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator after rotation', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret2)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) - }) - }) - - t.test('unsignCookie via reply decorator after rotation', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', secret2)}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: 'foo', renew: true, valid: true } }) - }) - }) - - t.test('unsignCookie via request decorator failure response', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: req.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) - }) - }) - - t.test('unsignCookie reply decorator failure response', t => { - t.plan(3) - const fastify = Fastify() - const secret1 = 'sec-1' - const secret2 = 'sec-2' - fastify.register(plugin, { secret: [secret1, secret2] }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}` - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: { value: null, renew: false, valid: false } }) - }) - }) -}) - -test('custom signer', t => { - t.plan(7) - const fastify = Fastify() - const signStub = sinon.stub().returns('SIGNED-VALUE') - const unsignStub = sinon.stub().returns('ORIGINAL VALUE') - const secret = { sign: signStub, unsign: unsignStub } - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply - .setCookie('foo', 'bar', { signed: true }) - .send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - const cookies = res.cookies - t.equal(cookies.length, 1) - t.equal(cookies[0].name, 'foo') - t.equal(cookies[0].value, 'SIGNED-VALUE') - t.ok(signStub.calledOnceWithExactly('bar')) - }) -}) - -test('unsignCookie decorator with custom signer', t => { - t.plan(4) - const fastify = Fastify() - const signStub = sinon.stub().returns('SIGNED-VALUE') - const unsignStub = sinon.stub().returns('ORIGINAL VALUE') - const secret = { sign: signStub, unsign: unsignStub } - fastify.register(plugin, { secret }) - - fastify.get('/test1', (req, reply) => { - reply.send({ - unsigned: reply.unsignCookie(req.cookies.foo) - }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: 'foo=SOME-SIGNED-VALUE' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { unsigned: 'ORIGINAL VALUE' }) - t.ok(unsignStub.calledOnceWithExactly('SOME-SIGNED-VALUE')) - }) -}) - -test('pass options to `cookies.parse`', (t) => { - t.plan(6) - const fastify = Fastify() - fastify.register(plugin, { - parseOptions: { - decode: decoder - } - }) - - fastify.get('/test1', (req, reply) => { - t.ok(req.cookies) - t.ok(req.cookies.foo) - t.equal(req.cookies.foo, 'bartest') - reply.send({ hello: 'world' }) - }) - - fastify.inject({ - method: 'GET', - url: '/test1', - headers: { - cookie: 'foo=bar' - } - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - }) - - function decoder (str) { - return str + 'test' - } -}) - -test('issue 53', (t) => { - t.plan(5) - const fastify = Fastify() - fastify.register(plugin) - - let cookies - let count = 1 - fastify.get('/foo', (req, reply) => { - if (count > 1) { - t.not(cookies, req.cookies) - return reply.send('done') - } - - count += 1 - cookies = req.cookies - reply.send('done') - }) - - fastify.inject({ url: '/foo' }, (err, response) => { - t.error(err) - t.equal(response.body, 'done') - }) - - fastify.inject({ url: '/foo' }, (err, response) => { - t.error(err) - t.equal(response.body, 'done') - }) -}) - -test('parse cookie manually using decorator', (t) => { - t.plan(2) - const fastify = Fastify() - fastify.register(plugin) - - fastify.ready(() => { - t.ok(fastify.parseCookie) - t.same(fastify.parseCookie('foo=bar', {}), { foo: 'bar' }) - t.end() - }) -})