Skip to content

Commit 47fff20

Browse files
committed
fix: fix cookie rules update strategy
1 parent 3ad8c2c commit 47fff20

File tree

8 files changed

+529
-501
lines changed

8 files changed

+529
-501
lines changed

src/handler/cookie.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { textTester, urlTester } from '@/common/util';
2+
import { CookieData, CookieMatchResult } from '@/types';
3+
import { debounce, pick } from 'es-toolkit';
4+
import browser from 'webextension-polyfill';
5+
import { dataLoaded } from './data';
6+
import { getUrl } from './util';
7+
8+
let cookieRules: CookieData[] = [];
9+
let processing = false;
10+
const updates = new Map<string, browser.Cookies.SetDetailsType>();
11+
let dataReady: Promise<void>;
12+
13+
const debouncedReload = debounce(reloadRules, 500);
14+
const updateCookiesLater = debounce(updateCookies, 100);
15+
16+
export const cookieActions = {
17+
reload: debouncedReload,
18+
};
19+
20+
browser.cookies.onChanged.addListener(handleCookieChange);
21+
22+
async function reloadRules() {
23+
const lists = await dataLoaded;
24+
cookieRules = lists.cookie
25+
.filter((list) => list.enabled)
26+
.flatMap((list) => list.rules.filter((rule) => rule.enabled));
27+
console.info(`[cookie] rules reloaded (${cookieRules.length})`);
28+
}
29+
30+
async function handleCookieChange(
31+
changeInfo: browser.Cookies.OnChangedChangeInfoType,
32+
) {
33+
// if (['expired', 'evicted'].includes(changeInfo.cause)) return;
34+
if (changeInfo.cause !== 'explicit') return;
35+
if (processing) return;
36+
await (dataReady ||= reloadRules());
37+
let update: CookieMatchResult | undefined;
38+
for (const rule of cookieRules) {
39+
const url = getUrl(changeInfo.cookie);
40+
const matches = url.match(urlTester(rule.url));
41+
if (!matches) continue;
42+
if (rule.name && !changeInfo.cookie.name.match(textTester(rule.name)))
43+
continue;
44+
const { ttl } = rule;
45+
if (changeInfo.removed && !(ttl && ttl > 0)) {
46+
// If cookie is removed and no positive ttl, ignore since change will not persist
47+
continue;
48+
}
49+
update = pick(rule, ['sameSite', 'httpOnly', 'secure']);
50+
if (ttl != null) {
51+
// If ttl is 0, set to undefined to mark the cookie as a session cookie
52+
update.expirationDate = ttl
53+
? Math.floor(Date.now() / 1000 + ttl)
54+
: undefined;
55+
}
56+
if (update.sameSite === 'no_restriction') update.secure = true;
57+
break;
58+
}
59+
if (update) {
60+
const { cookie } = changeInfo;
61+
const hasUpdate = Object.entries(update).some(([key, value]) => {
62+
return cookie[key as keyof browser.Cookies.Cookie] !== value;
63+
});
64+
if (!hasUpdate) {
65+
console.info(`[cookie] no update: ${cookie.name} ${getUrl(cookie)}`);
66+
return;
67+
}
68+
const details: browser.Cookies.SetDetailsType = {
69+
url: getUrl(pick(cookie, ['domain', 'path', 'secure'])),
70+
domain: cookie.hostOnly ? undefined : cookie.domain,
71+
expirationDate: cookie.session ? undefined : cookie.expirationDate,
72+
...pick(cookie, [
73+
'name',
74+
'path',
75+
'httpOnly',
76+
'sameSite',
77+
'secure',
78+
'storeId',
79+
'value',
80+
]),
81+
...update,
82+
};
83+
console.info(`[cookie] matched: ${details.name} ${details.url}`, details);
84+
updates.set(
85+
[details.storeId, details.url, details.name].join('\n'),
86+
details,
87+
);
88+
updateCookiesLater();
89+
}
90+
}
91+
92+
async function updateCookies() {
93+
if (processing) return;
94+
processing = true;
95+
const items = Array.from(updates.values());
96+
updates.clear();
97+
for (const item of items) {
98+
console.info(`[cookie] set: ${item.name} ${item.url}`, item);
99+
try {
100+
await browser.cookies.set(item);
101+
} catch (err) {
102+
console.error(err);
103+
}
104+
}
105+
processing = false;
106+
}

src/handler/data.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { normalizeCookieRule, normalizeRequestRule } from '@/common/list';
2+
import type { ListData, ListGroups } from '@/types';
3+
import { flatMap, groupBy } from 'es-toolkit';
4+
import browser from 'webextension-polyfill';
5+
import { dumpExactData, getExactData } from './util';
6+
7+
const LIST_PREFIX = 'list:';
8+
const KEY_LISTS = 'lists';
9+
10+
let lastId = -1;
11+
12+
export const dataLoaded = loadData();
13+
14+
export async function loadData() {
15+
let ids = await getExactData<number[]>(KEY_LISTS);
16+
const lists: ListGroups = { request: [], cookie: [] };
17+
if (Array.isArray(ids)) {
18+
const allData = await browser.storage.local.get(
19+
ids.map((id) => `${LIST_PREFIX}${id}`),
20+
);
21+
const allLists = ids
22+
.map((id) => allData[`${LIST_PREFIX}${id}`])
23+
.filter(Boolean) as ListData[];
24+
const groups = groupBy(allLists, (item) => item.type);
25+
Object.assign(lists, groups);
26+
} else {
27+
const allData = await browser.storage.local.get();
28+
const allLists = Object.keys(allData)
29+
.filter((key) => key.startsWith(LIST_PREFIX))
30+
.map((key) => allData[key]) as ListData[];
31+
const groups = groupBy(allLists, (item) => item.type);
32+
Object.assign(lists, groups);
33+
}
34+
if (import.meta.env.DEV) console.log('loadData:raw', lists);
35+
ids = Object.values(lists).flatMap((group: ListData[]) =>
36+
group.map((item) => item.id),
37+
);
38+
lastId = Math.max(0, ...ids);
39+
lists.request.forEach((list) => {
40+
list.enabled ??= true;
41+
list.rules = flatMap(list.rules, normalizeRequestRule);
42+
});
43+
lists.cookie.forEach((list) => {
44+
list.enabled ??= true;
45+
list.rules = flatMap(list.rules, normalizeCookieRule);
46+
});
47+
if (import.meta.env.DEV) console.log('loadData', lists);
48+
return lists;
49+
}
50+
51+
export function getKey(id: number) {
52+
return `${LIST_PREFIX}${id}`;
53+
}
54+
55+
export async function dumpLists(lists: ListGroups) {
56+
await dumpExactData(
57+
KEY_LISTS,
58+
Object.values(lists).flatMap((group: ListData[]) =>
59+
group.map((group) => group.id),
60+
),
61+
);
62+
}
63+
64+
export async function saveList(data: Partial<ListData>) {
65+
const list: ListData = {
66+
id: 0,
67+
name: 'No name',
68+
subscribeUrl: '',
69+
lastUpdated: 0,
70+
enabled: true,
71+
type: 'request',
72+
rules: [],
73+
...data,
74+
};
75+
if (!list.rules) throw new Error('Invalid list data');
76+
list.name ||= 'No name';
77+
if (!list.id) {
78+
if (lastId < 0) throw new Error('Data is not loaded yet');
79+
list.id = ++lastId;
80+
}
81+
if (import.meta.env.DEV) console.log('saveList', list);
82+
await dumpExactData(getKey(list.id), list);
83+
return list;
84+
}

0 commit comments

Comments
 (0)