-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi.mjs
342 lines (308 loc) · 10.4 KB
/
api.mjs
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
/* global ChromeUtils, ExtensionAPI, Services */
const { ExtensionPreferencesManager } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
);
const { getSettingsAPI } = ExtensionPreferencesManager;
const { RFPHelper } = ChromeUtils.importESModule(
"resource://gre/modules/RFPHelper.sys.mjs"
);
const { ForgetAboutSite } = ChromeUtils.importESModule(
"resource://gre/modules/ForgetAboutSite.sys.mjs"
);
const DISABLED_TARGETS = new Set([
"IsAlwaysEnabledForPrecompute",
"AllTargets",
]);
const TARGETS = new Set(Object.keys(RFPHelper.getTargets())).difference(
DISABLED_TARGETS
);
const DEFAULT_TARGETS = new Set(RFPHelper.getTargetDefaults());
const OverridesHelper = {
// Parses a string of the form "+target1,-target2,+target3"
parse(str) {
const targets = {};
const invalid = [];
if (!str || str.length === 0) {
return { targets, invalid };
}
for (const targetStr of str.split(",").map((s) => s.trim())) {
const [op, target] = [targetStr.slice(0, 1), targetStr.slice(1)];
if (!OverridesHelper.validateEntry(op, target)) {
invalid.push(targetStr);
continue;
}
targets[target] = op === "+";
}
return { targets, invalid };
},
// Validates an entry of the form "+target" or "-target"
validateEntry(op, target) {
return ["-", "+"].includes(op) && TARGETS.has(target);
},
// Validates a target and throws an error if it is invalid
validateTarget(target) {
if (!TARGETS.has(target)) {
throw new Error("Invalid target");
}
},
// Validate domain and throw an error if it is invalid
validateDomain(domain) {
if (!domain || domain.length === 0) {
throw new Error("No domain");
}
if (domain === "*") {
return;
}
const uri = Services.io.newURI("https://" + domain);
if (uri.displayHost !== domain) {
throw new Error("Invalid domain");
}
if (!uri.schemeIs("https")) {
throw new Error("Invalid spec");
}
},
// Serializes a map of targets to a string
stringify(targets) {
return Object.entries(targets)
.map(([target, enabled]) => (enabled ? "+" : "-") + target)
.join(",");
},
// Filters negative targets IFF they are not in the default set
filterNegatives(targets) {
return Object.fromEntries(
Object.entries(targets).filter(
([t, enabled]) => DEFAULT_TARGETS.has(t) || enabled
)
);
},
// Parses a granular overrides string of the form '[{firstPartyDomain: "example.com", thirdPartyDomain: "*", overrides: "+target1,-target2"}]'
parseGranular: function (str) {
const entries = [];
if (!str || str.length === 0) {
return entries;
}
const json = Utils.tryParseJSON(str, []);
for (const entry of json) {
const overrides = OverridesHelper.parse(entry.overrides).targets;
entries.push({
firstPartyDomain: entry.firstPartyDomain ?? "*",
thirdPartyDomain: entry.thirdPartyDomain ?? "*",
overrides: overrides,
});
}
return entries;
},
// Serializes a granular overrides array to a string
stringifyGranular(entries) {
return JSON.stringify(
entries.map((entry) => {
return {
firstPartyDomain: entry.firstPartyDomain,
thirdPartyDomain: entry.thirdPartyDomain,
overrides: OverridesHelper.stringify(entry.overrides),
};
})
);
},
// Adds default targets to an overrides object and returns it. Does not modify the input object.
appendDefaults(overrides) {
return Object.assign(
Object.fromEntries([...DEFAULT_TARGETS].map((t) => [t, true])),
{ ...overrides }
);
},
};
const Utils = {
// Tries to parse a JSON string, returns the default value if it fails
tryParseJSON(str, defaultValue) {
try {
return JSON.parse(str);
} catch (e) {
return defaultValue;
}
},
};
const ExtensionPrefHelper = {
// Adds a setting to the ExtensionPreferencesManager
// This enables setting the prefs back to its previous values when the extension is uninstalled
addSetting(name, pref, type) {
ExtensionPreferencesManager.addSetting(name, {
prefNames: [pref],
setCallback(value) {
return {
[pref]: value,
};
},
getCallback() {
return Services.prefs[`get${type}Pref`](pref);
},
});
},
// Returns an object with get and set functions for each setting
getAPIs(context, names) {
return names.reduce((acc, name) => {
const api = getSettingsAPI({
context,
name,
});
acc[name] = {
set: (value) => api.set({ value }),
get: () => api.get(name).then((r) => r.value),
};
return acc;
}, {});
},
};
const FPP_NAME = "fingerprintingProtection";
const FPP_PREF = "privacy.fingerprintingProtection";
const OVERRIDES_NAME = "fingerprintingProtection.overrides";
const OVERRIDES_PREF = "privacy.fingerprintingProtection.overrides";
const GRANULAR_OVERRIDES_NAME = "fingerprintingProtection.granularOverrides";
const GRANULAR_OVERRIDES_PREF =
"privacy.fingerprintingProtection.granularOverrides";
ExtensionPrefHelper.addSetting(OVERRIDES_NAME, OVERRIDES_PREF, "String");
ExtensionPrefHelper.addSetting(FPP_NAME, FPP_PREF, "Bool");
ExtensionPrefHelper.addSetting(
GRANULAR_OVERRIDES_NAME,
GRANULAR_OVERRIDES_PREF,
"String"
);
this.fppOverrides = class extends ExtensionAPI {
getAPI(context) {
// Create pref APIs for the FPP, overrides, and granular overrides prefs
const extAPI = (() => {
const apis = ExtensionPrefHelper.getAPIs(context, [
FPP_NAME,
OVERRIDES_NAME,
GRANULAR_OVERRIDES_NAME,
]);
return {
fpp: apis[FPP_NAME],
overrides: apis[OVERRIDES_NAME],
granular: apis[GRANULAR_OVERRIDES_NAME],
};
})();
const setTargetsByScope = async (overrides, domain, isGranular) => {
if (isGranular) {
const entries = await extAPI.granular
.get()
.then(OverridesHelper.parseGranular);
const entryI = entries.findIndex((e) => e.firstPartyDomain === domain);
if (entryI === -1) {
entries.push({
firstPartyDomain: domain,
thirdPartyDomain: "*",
overrides: {},
});
}
// Remove the entry the overrides are empty
const isEmpty = Object.keys(overrides).length === 0;
if (isEmpty) {
entries.splice(entryI === -1 ? entries.length - 1 : entryI, 1);
await extAPI.granular.set(OverridesHelper.stringifyGranular(entries));
return;
}
const entry = entries[entryI === -1 ? entries.length - 1 : entryI];
entry.overrides = overrides;
await extAPI.granular.set(OverridesHelper.stringifyGranular(entries));
} else {
await extAPI.overrides.set(
OverridesHelper.stringify(OverridesHelper.filterNegatives(overrides))
);
}
};
return {
fppOverrides: {
// Enables privacy.fingerprintingProtection
async enable() {
await extAPI.fpp.set(true);
},
// Returns privacy.fingerprintingProtection
async enabled() {
return extAPI.fpp.get();
},
// Reads and parses privacy.fingerprintingProtection.overrides
async get(domain) {
OverridesHelper.validateDomain(domain);
const global = await extAPI.overrides
.get()
.then(OverridesHelper.parse)
.then((r) => OverridesHelper.appendDefaults(r.targets));
const granular = await (async () => {
const entries = await extAPI.granular
.get()
.then(OverridesHelper.parseGranular);
const entry = entries.find((e) => e.firstPartyDomain === domain);
if (entry) {
return entry.overrides;
}
return {};
})();
return {
global,
granular,
};
},
// Modifies global or granular overrides to enable or disable a target
async set(target, enabled, domain, isGranular) {
OverridesHelper.validateTarget(target);
OverridesHelper.validateDomain(domain);
const overrides = await this.get(domain);
const targets = isGranular ? overrides.granular : overrides.global;
targets[target] = enabled;
await setTargetsByScope(targets, domain, isGranular);
},
// Modifies overrides to enable or disable all of the targets
async setAll(enabled, domain, isGranular) {
OverridesHelper.validateDomain(domain);
const overrides = Object.fromEntries(
[...TARGETS].map((t) => [t, enabled])
);
await setTargetsByScope(overrides, domain, isGranular);
},
// Removes a target from the overrides. Unlike set, this function will not add -Target to overrides
async remove(target, domain, isGranular) {
OverridesHelper.validateTarget(target);
OverridesHelper.validateDomain(domain);
const overrides = await this.get(domain);
const targets = isGranular ? overrides.granular : overrides.global;
delete targets[target];
await setTargetsByScope(targets, domain, isGranular);
},
// Clears all the overrides
async clear(domain, isGranular) {
OverridesHelper.validateDomain(domain);
await setTargetsByScope({}, domain, isGranular);
},
// Modifies overrides to only enable defaults
async resetToDefaults(domain, isGranular) {
OverridesHelper.validateDomain(domain);
await setTargetsByScope(
OverridesHelper.appendDefaults({}),
domain,
isGranular
);
},
// Forgets the website
async forgetWebsite(domain) {
OverridesHelper.validateDomain(domain);
await ForgetAboutSite.removeDataFromBaseDomain(domain);
},
// Validates the global overrides and returns unknown targets
invalids() {
return extAPI.overrides
.get()
.then((s) => OverridesHelper.parse(s).invalid);
},
// Returns a list of available targets
available() {
return [...TARGETS];
},
// Returns a list of default targets
defaults() {
return DEFAULT_TARGETS;
},
},
};
}
};