diff --git a/cookie.js b/cookie.js deleted file mode 100644 index a019803..0000000 --- a/cookie.js +++ /dev/null @@ -1,247 +0,0 @@ -/*! - * 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' - -/** - * 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} [opt] - * @return {object} - * @public - */ - -function parse (str, opt) { - if (typeof str !== 'string') { - throw new TypeError('argument str must be a string') - } - - const dec = opt?.decode || decodeURIComponent - const result = {} - - const strLen = str.length - let pos = 0 - let terminatorPos = 0 - while (true) { - if (terminatorPos === strLen) break - terminatorPos = str.indexOf(';', pos) - if (terminatorPos === -1) terminatorPos = strLen // This is the last pair - - let eqIdx = str.indexOf('=', pos) - if (eqIdx === -1) break // No key-value pairs left - if (eqIdx > terminatorPos) { - // Malformed key-value pair - pos = terminatorPos + 1 - continue - } - - const key = str.substring(pos, eqIdx++).trim() - if (result[key] === undefined) { - const val = str.charCodeAt(eqIdx) === 0x22 - ? str.substring(eqIdx + 1, terminatorPos - 1).trim() - : str.substring(eqIdx, terminatorPos).trim() - - result[key] = !(dec === decodeURIComponent && val.indexOf('%') === -1) - ? tryDecode(val, dec) - : val - } - - pos = terminatorPos + 1 - } - - return result -} - -/** - * 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} [opt] - * @return {string} - * @public - */ - -function serialize (name, val, opt) { - const enc = opt?.encode || encodeURIComponent - if (typeof enc !== 'function') { - throw new TypeError('option encode is invalid') - } - - if (name && !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 == null) return str - - if (opt.maxAge != null) { - const maxAge = +opt.maxAge - - if (!isFinite(maxAge)) { - throw new TypeError('option maxAge is invalid') - } - - str += '; Max-Age=' + Math.trunc(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.priority) { - const priority = typeof opt.priority === 'string' - ? opt.priority.toLowerCase() - : opt.priority - - switch (priority) { - case 'low': - str += '; Priority=Low' - break - case 'medium': - str += '; Priority=Medium' - break - case 'high': - str += '; Priority=High' - break - default: - throw new TypeError('option priority is invalid') - } - } - - 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' - } - - // Draft implementation to support Chrome from 2024-Q1 forward. - // See https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1 - if (opt.partitioned) { - str += '; Partitioned' - } - - 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 - * @returns {string} - * @private - */ -function tryDecode (str, decode) { - try { - return decode(str) - } catch { - return str - } -} - -/** - * Module exports. - * @public - */ - -module.exports = { - parse, - serialize -} diff --git a/package.json b/package.json index fc94f74..91e9226 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,9 @@ "tsd": "^0.31.1" }, "dependencies": { - "fastify-plugin": "^5.0.0", - "cookie-signature": "^1.2.1" + "cookie": "^1.0.0", + "cookie-signature": "^1.2.1", + "fastify-plugin": "^5.0.0" }, "tsd": { "directory": "test" diff --git a/plugin.js b/plugin.js index 8aec1ca..232f4b0 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 { Signer, sign, unsign } = require('./signer') diff --git a/test/cookie-module.test.js b/test/cookie-module.test.js deleted file mode 100644 index c38dea0..0000000 --- a/test/cookie-module.test.js +++ /dev/null @@ -1,223 +0,0 @@ -'use strict' - -const tap = require('tap') -const test = tap.test - -const cookie = require('..') - -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() -}) - -test('serializer: priority', (t) => { - t.plan(8) - t.same(cookie.serialize('foo', 'bar', { priority: 'Low' }), 'foo=bar; Priority=Low') - t.same(cookie.serialize('foo', 'bar', { priority: 'low' }), 'foo=bar; Priority=Low') - t.same(cookie.serialize('foo', 'bar', { priority: 'Medium' }), 'foo=bar; Priority=Medium') - t.same(cookie.serialize('foo', 'bar', { priority: 'medium' }), 'foo=bar; Priority=Medium') - t.same(cookie.serialize('foo', 'bar', { priority: 'High' }), 'foo=bar; Priority=High') - t.same(cookie.serialize('foo', 'bar', { priority: 'high' }), 'foo=bar; Priority=High') - - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - priority: 'foo' - }), /option priority is invalid/) - t.throws(cookie.serialize.bind(cookie, 'foo', 'bar', { - priority: true - }), /option priority is invalid/) - t.end() -})