Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support set partitioned property on Chrome >= 114 #42

Merged
merged 2 commits into from Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/nodejs.yml
Expand Up @@ -7,8 +7,6 @@ on:
pull_request:
branches: [ master ]

workflow_dispatch: {}

jobs:
Job:
name: Node.js
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/release.yml
Expand Up @@ -4,14 +4,10 @@ on:
push:
branches: [ master ]

workflow_dispatch: {}

jobs:
release:
name: Node.js
uses: node-modules/github-actions/.github/workflows/node-release.yml@master
uses: eggjs/github-actions/.github/workflows/node-release.yml@master
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
with:
checkTest: false
3 changes: 2 additions & 1 deletion README.zh-CN.md
Expand Up @@ -36,7 +36,8 @@ ctx.cookies.set('key', 'value', options);
- domain - `String` cookie 的有效域名范围,默认为 `undefined`。
- expires - `Date` cookie 的失效时间。
- maxAge - `Number` cookie 的最大有效时间,如果设置了 maxAge,将会覆盖 expires 的值。
- secure - `Boolean` 是否只在加密信道中传输,注意,如果请求为 http 时,不允许设置为 true https 时自动设置为 ture。
- secure - `Boolean` 是否只在加密信道中传输,注意,如果请求为 http 时,不允许设置为 true https 时自动设置为 true。
- partitioned - `Boolean` 是否设置独立分区状态([CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips))的 Cookie。注意,只有 `secure` 为 true 的时候此配置才会生效。
- httpOnly - `Boolean` 如果设置为 ture,则浏览器中不允许读取这个 cookie 的值。
- overwrite - `Boolean` 如果设置为 true,在一个请求上重复写入同一个 key 将覆盖前一次写入的值,默认为 false。
- signed - `Boolean` 是否需要对 cookie 进行签名,需要配合 get 时传递 signed 参数,此时前端无法篡改这个 cookie,默认为 true。
Expand Down
4 changes: 3 additions & 1 deletion lib/cookie.js
Expand Up @@ -59,19 +59,21 @@ class Cookie {
if (attrs.sameSite) header += '; samesite=' + (attrs.sameSite === true ? 'strict' : attrs.sameSite.toLowerCase());
if (attrs.secure) header += '; secure';
if (attrs.httpOnly) header += '; httponly';
if (attrs.partitioned) header += '; partitioned';

return header;
}
}

const ATTRS = [ 'path', 'expires', 'domain', 'httpOnly', 'secure', 'maxAge', 'overwrite', 'sameSite' ];
const ATTRS = [ 'path', 'expires', 'domain', 'httpOnly', 'secure', 'partitioned', 'maxAge', 'overwrite', 'sameSite' ];
function mergeDefaultAttrs(attrs) {
const merged = {
path: '/',
httpOnly: true,
secure: false,
overwrite: false,
sameSite: false,
partitioned: false,
};
if (!attrs) return merged;

Expand Down
26 changes: 24 additions & 2 deletions lib/cookies.js
Expand Up @@ -9,6 +9,7 @@ const CookieError = require('./error');

const KEYS_ARRAY = Symbol('eggCookies:keysArray');
const KEYS = Symbol('eggCookies:keys');
const PARSED_UA = Symbol('eggCookies:parsedUA');
const keyCache = new Map();

/**
Expand Down Expand Up @@ -113,13 +114,19 @@ class Cookies {

// https://github.com/linsight/should-send-same-site-none
// fixed SameSite=None: Known Incompatible Clients
const userAgent = this.ctx.get('user-agent');
if (opts.sameSite && typeof opts.sameSite === 'string' && opts.sameSite.toLowerCase() === 'none') {
const userAgent = this.ctx.get('user-agent');
if (!this.secure || (userAgent && !this.isSameSiteNoneCompatible(userAgent))) {
// Non-secure context or Incompatible clients, don't send SameSite=None property
opts.sameSite = false;
}
}
if (opts.partitioned) {
if (!this.secure || (userAgent && !this.isPartitionedCompatible(userAgent))) {
// Non-secure context or Incompatible clients, don't send partitioned property
opts.partitioned = false;
}
}

const cookie = new Cookie(name, value, opts);

Expand All @@ -139,12 +146,27 @@ class Cookies {
return this;
}

_parseChromiumAndMajorVersion(userAgent) {
if (!this[PARSED_UA]) {
this[PARSED_UA] = parseChromiumAndMajorVersion(userAgent);
}
return this[PARSED_UA];
}

isSameSiteNoneCompatible(userAgent) {
// Chrome >= 80.0.0.0
const result = parseChromiumAndMajorVersion(userAgent);
const result = this._parseChromiumAndMajorVersion(userAgent);
if (result.chromium) return result.majorVersion >= 80;
return _isSameSiteNoneCompatible(userAgent);
}

isPartitionedCompatible(userAgent) {
// Chrome >= 114.0.0.0
// https://developers.google.com/privacy-sandbox/3pcd/chips
const result = this._parseChromiumAndMajorVersion(userAgent);
if (result.chromium) return result.majorVersion >= 114;
return false;
}
}

// https://github.com/linsight/should-send-same-site-none/blob/master/index.js#L86
Expand Down
127 changes: 125 additions & 2 deletions test/lib/cookies.test.js
Expand Up @@ -296,7 +296,7 @@ describe('test/lib/cookies.test.js', () => {
}
});

it('should send not SameSite=None property on Chrome < 80', () => {
it('should not send SameSite=None property on Chrome < 80', () => {
const cookies = Cookies({
secure: true,
headers: {
Expand All @@ -316,7 +316,7 @@ describe('test/lib/cookies.test.js', () => {
}
});

it('should send not SameSite=None property on Chrome >= 80', () => {
it('should send SameSite=None property on Chrome >= 80', () => {
let cookies = Cookies({
secure: true,
headers: {
Expand Down Expand Up @@ -391,4 +391,127 @@ describe('test/lib/cookies.test.js', () => {
assert(str.includes('; path=/; httponly'));
}
});

describe('opts.partitioned', () => {
it('should not send partitioned property on incompatible clients', () => {
const userAgents = [
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML%2C like Gecko) Chrome/64.0.3282.140 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36',
'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-CN; OE106 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML%2C like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/11.9.4.974 UWS/2.13.2.90 Mobile Safari/537.36 AliApp(DingTalk/4.7.18) com.alibaba.android.rimet/12362010 Channel/1565683214685 language/zh-CN UT4Aplus/0.2.25',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML%2C like Gecko) Chrome/63.0.3239.132 Safari/537.36 dingtalk-win/1.0.0 nw(0.14.7) DingTalk(4.7.19-Release.16) Mojo/1.0.0 Native AppType(release)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML%2C like Gecko) Chrome/62.0.3202.94 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML%2C like Gecko) Chrome/52.0.2723.2 Safari/537.36',
];
for (const ua of userAgents) {
const cookies = Cookies({
secure: true,
headers: {
'user-agent': ua,
},
}, { secure: true }, { partitioned: true });
const opts = {
signed: 1,
};
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; secure; httponly'));
}
}
});

it('should not send partitioned property on Chrome < 114', () => {
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/113.0.3945.29 Safari/537.36',
},
}, { secure: true }, { partitioned: true });
const opts = {
signed: 1,
};
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; secure; httponly'));
}
});

it('should send partitioned property on Chrome >= 114', () => {
let 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 });
const opts = {
signed: 1,
};
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; secure; httponly; partitioned'));
}

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/115.0.3945.29 Safari/537.36',
},
}, { secure: true }, { partitioned: true });
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; secure; httponly; partitioned'));
}

// empty user-agent
cookies = Cookies({
secure: true,
headers: {
'user-agent': '',
},
}, { secure: true }, { partitioned: true });
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; secure; httponly; partitioned'));
}
});

it('should not send SameSite=none property on non-secure context', () => {
const cookies = Cookies({
secure: false,
headers: {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.3945.29 Safari/537.36',
},
}, null, { partitioned: true });
const opts = {
signed: 1,
};
cookies.set('foo', 'hello', opts);

assert(opts.signed === 1);
assert(opts.secure === undefined);
assert(cookies.ctx.response.headers['set-cookie'].join(';').match(/foo=hello/));
for (const str of cookies.ctx.response.headers['set-cookie']) {
assert(str.includes('; path=/; httponly'));
}
});
});
});