-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.js
More file actions
163 lines (148 loc) · 5.25 KB
/
Copy pathbackground.js
File metadata and controls
163 lines (148 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// GPC Watchdog — background detection engine (Firefox, MV2)
//
// Per-tab pipeline:
// 1. Main-frame request → confirm the Sec-GPC: 1 header actually went out
// 2. Every subsequent request in that tab → flag third-party hits to known
// ad-tech domains (only meaningful if GPC was sent)
// 3. Page load complete → sweep cookies for known tracking cookies
// 4. Content script reports navigator.globalPrivacyControl and any CMP
// consent state (TCF / USP APIs)
//
// A "violation" here means: GPC was verifiably sent, and the site loaded
// ad-tech anyway. That's an *indicator* the opt-out wasn't honored — the
// complaint language reflects that precision. The extension never submits
// anything; it prepares evidence for the user to review and file.
const tabState = new Map();
function freshState(url) {
let origin = "";
try {
origin = new URL(url).hostname;
} catch (e) {}
return {
origin,
url,
startedAt: new Date().toISOString(),
gpcHeaderSent: false,
gpcNavigatorFlag: null, // reported by content script
cmp: null, // consent state reported by content script
trackerHits: new Map(), // hostname → { count, firstSeen, types, category, entity }
trackingCookies: [],
};
}
// --- 1. Verify the GPC header on the main-frame request -------------------
browser.webRequest.onBeforeSendHeaders.addListener(
(details) => {
if (details.type !== "main_frame" || details.tabId < 0) return;
const state = freshState(details.url);
state.gpcHeaderSent = (details.requestHeaders || []).some(
(h) => h.name.toLowerCase() === "sec-gpc" && h.value === "1"
);
tabState.set(details.tabId, state);
updateBadge(details.tabId);
},
{ urls: ["<all_urls>"] },
["requestHeaders"]
);
// --- 2. Watch every request in the tab for ad-tech domains ----------------
browser.webRequest.onBeforeRequest.addListener(
(details) => {
if (details.tabId < 0 || details.type === "main_frame") return;
const state = tabState.get(details.tabId);
if (!state) return;
let host;
try {
host = new URL(details.url).hostname;
} catch (e) {
return;
}
// Third-party relative to the page (eTLD+1 comparison — see etld.js),
// and on the tracker list (Disconnect-generated + curated — see
// data/trackers.js)
const match = matchTrackerHost(host);
if (!match || sameSite(host, state.origin)) return;
const entry = state.trackerHits.get(host) || {
count: 0,
firstSeen: new Date().toISOString(),
types: new Set(),
category: match.category,
entity: match.entity,
};
entry.count += 1;
entry.types.add(details.type);
state.trackerHits.set(host, entry);
updateBadge(details.tabId);
},
{ urls: ["<all_urls>"] }
);
// --- 3. Cookie sweep after the page settles -------------------------------
browser.webNavigation.onCompleted.addListener(async (details) => {
if (details.frameId !== 0) return;
const state = tabState.get(details.tabId);
if (!state) return;
// Small delay: many pixels set cookies just after load
await new Promise((r) => setTimeout(r, 2500));
try {
const cookies = await browser.cookies.getAll({ url: state.url });
state.trackingCookies = cookies
.map((c) => ({ cookie: c, match: matchTrackingCookie(c.name) }))
.filter((x) => x.match)
.map((x) => ({
name: x.cookie.name,
domain: x.cookie.domain,
category: x.match.category,
vendor: x.match.vendor,
}));
updateBadge(details.tabId);
} catch (e) {
// cookies permission failure — leave list empty
}
});
// --- 4. Reports from the content script -----------------------------------
browser.runtime.onMessage.addListener((msg, sender) => {
if (msg.kind === "page-report" && sender.tab) {
const state = tabState.get(sender.tab.id);
if (state) {
state.gpcNavigatorFlag = msg.gpcNavigatorFlag;
state.cmp = msg.cmp;
}
return;
}
if (msg.kind === "get-state") {
const state = tabState.get(msg.tabId);
return Promise.resolve(state ? serialize(state) : null);
}
});
// The watching-eye icon ladder: grey closed eye (no-gpc, also the toolbar
// default) when the watcher can't verify — signal off or state unknown;
// colored closed eye (asleep) on a verified-clean page; open (awake) when
// tracking worth watching appeared (weak, analytics-only evidence);
// wide-eyed (alert) only when the filing pipeline is armed.
const VERDICT_ICONS = {
"no-gpc": "no-gpc",
clean: "asleep",
"weak-indicators": "awake",
indicators: "alert",
};
function updateBadge(tabId) {
const state = tabState.get(tabId);
if (!state) return;
const v = verdict(state);
const icon = VERDICT_ICONS[v] || "asleep";
browser.browserAction.setIcon({
tabId,
path: { 16: `icons/${icon}-16.png`, 32: `icons/${icon}-32.png` },
});
if (v === "indicators" || v === "weak-indicators") {
browser.browserAction.setBadgeText({
tabId,
text: badgeLabel(state.trackerHits.size),
});
browser.browserAction.setBadgeBackgroundColor({
tabId,
color: v === "indicators" ? "#C9552E" : "#8A93A0",
});
} else {
browser.browserAction.setBadgeText({ tabId, text: "" });
}
}
browser.tabs.onRemoved.addListener((tabId) => tabState.delete(tabId));