From b81f04181e461f2688296e4bd65cad8ac3a8298d Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 28 Dec 2023 16:16:21 +0800 Subject: [PATCH] fix: support remove unpartitioned same name cookie first (#44) --- README.zh-CN.md | 1 + index.d.ts | 4 ++++ lib/cookies.js | 32 +++++++++++++++++++++++++--- test/lib/cookies.test.js | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/README.zh-CN.md b/README.zh-CN.md index e19b4f2..5efed26 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -38,6 +38,7 @@ ctx.cookies.set('key', 'value', options); - maxAge - `Number` cookie 的最大有效时间,如果设置了 maxAge,将会覆盖 expires 的值。 - secure - `Boolean` 是否只在加密信道中传输,注意,如果请求为 http 时,不允许设置为 true https 时自动设置为 true。 - partitioned - `Boolean` 是否设置独立分区状态([CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips))的 Cookie。注意,只有 `secure` 为 true 的时候此配置才会生效。 +- removeUnpartitioned - `Boolean` 是否删除非独立分区状态的同名 cookie。注意,只有 `partitioned` 为 true 的时候此配置才会生效。 - httpOnly - `Boolean` 如果设置为 ture,则浏览器中不允许读取这个 cookie 的值。 - overwrite - `Boolean` 如果设置为 true,在一个请求上重复写入同一个 key 将覆盖前一次写入的值,默认为 false。 - signed - `Boolean` 是否需要对 cookie 进行签名,需要配合 get 时传递 signed 参数,此时前端无法篡改这个 cookie,默认为 true。 diff --git a/index.d.ts b/index.d.ts index 32de973..d258354 100644 --- a/index.d.ts +++ b/index.d.ts @@ -61,6 +61,10 @@ declare namespace EggCookies { * Is it partitioned or not. */ partitioned?: boolean; + /** + * Remove unpartitioned same name cookie or not. + */ + removeUnpartitioned?: boolean; } class CookieError extends Error { } diff --git a/lib/cookies.js b/lib/cookies.js index 825a34c..9dce3dc 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -128,11 +128,32 @@ class Cookies { } } - const cookie = new Cookie(name, value, opts); + // remove unpartitioned same name cookie first + if (opts.partitioned && opts.removeUnpartitioned) { + const overwrite = opts.overwrite; + if (overwrite) { + opts.overwrite = false; + headers = ignoreCookiesByName(headers, name); + } + const removeCookieOpts = Object.assign({}, opts, { + partitioned: false, + }); + const removeUnpartitionedCookie = new Cookie(name, '', removeCookieOpts); + // if user not set secure, reset secure to ctx.secure + if (opts.secure === undefined) removeUnpartitionedCookie.attrs.secure = this.secure; + + headers = pushCookie(headers, removeUnpartitionedCookie); + // signed + if (signed) { + removeUnpartitionedCookie.name += '.sig'; + headers = ignoreCookiesByName(headers, removeUnpartitionedCookie.name); + headers = pushCookie(headers, removeUnpartitionedCookie); + } + } + const cookie = new Cookie(name, value, opts); // if user not set secure, reset secure to ctx.secure if (opts.secure === undefined) cookie.attrs.secure = this.secure; - headers = pushCookie(headers, cookie); // signed @@ -199,11 +220,16 @@ function computeSigned(opts) { function pushCookie(cookies, cookie) { if (cookie.attrs.overwrite) { - cookies = cookies.filter(c => !c.startsWith(cookie.name + '=')); + cookies = ignoreCookiesByName(cookies, cookie.name); } cookies.push(cookie.toHeader()); return cookies; } +function ignoreCookiesByName(cookies, name) { + const prefix = `${name}=`; + return cookies.filter(c => !c.startsWith(prefix)); +} + Cookies.CookieError = CookieError; module.exports = Cookies; diff --git a/test/lib/cookies.test.js b/test/lib/cookies.test.js index 10adcee..d381ba4 100644 --- a/test/lib/cookies.test.js +++ b/test/lib/cookies.test.js @@ -513,5 +513,51 @@ describe('test/lib/cookies.test.js', () => { assert(str.includes('; path=/; httponly')); } }); + + it('should remove unpartitioned property first', () => { + const cookies = Cookies({ + secure: true, + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.3945.29 Safari/537.36', + }, + }, { secure: true }, { partitioned: true, removeUnpartitioned: true }); + const opts = { + signed: 1, + }; + cookies.set('foo', 'hello', opts); + + assert(opts.signed === 1); + assert(opts.secure === undefined); + const headers = cookies.ctx.response.headers['set-cookie']; + // console.log(headers); + assert.equal(headers.length, 4); + assert.equal(headers[0], 'foo=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; httponly'); + assert.equal(headers[1], 'foo.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; httponly'); + assert.equal(headers[2], 'foo=hello; path=/; secure; httponly; partitioned'); + assert.equal(headers[3], 'foo.sig=ZWbaA4bWk8ByBuYVgfmJ2DMvhhS3sOctMbfXAQ2vnwI; path=/; secure; httponly; partitioned'); + }); + + it('should remove unpartitioned property first with overwrite = true', () => { + const cookies = Cookies({ + secure: true, + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.3945.29 Safari/537.36', + }, + }, { secure: true }, { partitioned: true, removeUnpartitioned: true, overwrite: true }); + const opts = { + signed: 1, + }; + cookies.set('foo', 'hello2222', opts); + cookies.set('foo', 'hello', opts); + + assert(opts.signed === 1); + assert(opts.secure === undefined); + const headers = cookies.ctx.response.headers['set-cookie']; + assert.equal(headers.length, 4); + assert.equal(headers[0], 'foo=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; httponly'); + assert.equal(headers[1], 'foo.sig=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; httponly'); + assert.equal(headers[2], 'foo=hello; path=/; secure; httponly; partitioned'); + assert.equal(headers[3], 'foo.sig=ZWbaA4bWk8ByBuYVgfmJ2DMvhhS3sOctMbfXAQ2vnwI; path=/; secure; httponly; partitioned'); + }); }); });