Skip to content

Commit 355c42e

Browse files
Add support for container tabs (#218, thanks to @aaronkollasch for this huge contribution)
* Add support for container tabs * Hide contextStore checkbox if contextualIdentities is not available * Fix incorrect uses of default policy when a container-specific policy from the contextStore should be used instead * Always sync policy presets to contextStore policies * Localize user-facing container-related messages * Apply suggestions from code review Co-authored-by: Giorgio Maone <giorgio@maone.net> Co-authored-by: Aaron Kollasch <aaron@kollasch.dev> * Update src/ui/ui.js Fixed typo. Co-authored-by: Aaron Kollasch <aaron@kollasch.dev> --------- Co-authored-by: Giorgio Maone <giorgio@maone.net>
1 parent ecbba8f commit 355c42e

15 files changed

Lines changed: 426 additions & 65 deletions

src/_locales/en/messages.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,30 @@
823823
"TabGuard_optAllow": {
824824
"message": "Load normally"
825825
},
826+
"DefaultContainerName": {
827+
"message": "Default"
828+
},
829+
"enable_container_tabs_label": {
830+
"message": "Enable support for container tabs"
831+
},
832+
"select_container_label": {
833+
"message": "Choose a container:"
834+
},
835+
"copy_container_label": {
836+
"message": "Copy permissions from container:"
837+
},
838+
"clear_container_label": {
839+
"message": "Clear permissions for this container"
840+
},
841+
"forbid_replace_default_policy": {
842+
"message": "Cannot replace the default policy."
843+
},
844+
"container_copy_warning": {
845+
"message": "Copying permissions from \"$1\".\nAll site permissions for this container will be removed.\nThis action cannot be reverted.\nDo you want to continue?"
846+
},
847+
"container_clear_warning": {
848+
"message": "All site permissions for this container will be removed.\nThis action cannot be reverted.\nDo you want to continue?"
849+
},
826850
"LearnMoreLink": {
827851
"message": "Learn more…"
828852
},

src/bg/LifeCycle.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ var LifeCycle = (() => {
7070
));
7171

7272
const policy = ns.policy.dry(true);
73+
const contextStore = ns.contextStore.dry(true);
7374
const unrestrictedTabs = [...ns.unrestrictedTabs];
7475

7576
if (policy.sites.temp.length == 0 &&
@@ -118,6 +119,7 @@ var LifeCycle = (() => {
118119
let {url} = tab;
119120
let {cypherText, key, iv} = await encrypt(JSON.stringify({
120121
policy,
122+
contextStore,
121123
allSeen,
122124
unrestrictedTabs,
123125
}));
@@ -225,14 +227,15 @@ var LifeCycle = (() => {
225227
iv
226228
}, key, cypherText
227229
);
228-
let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
230+
let {policy, contextStore, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
229231
if (!policy) {
230232
throw new error("Ephemeral policy not found in survival tab %s!", tabId);
231233
}
232234
ns.unrestrictedTabs = new Set(unrestrictedTabs);
233235
destroyIfNeeded();
234236
if (ns.initializing) await ns.initializing;
235237
ns.policy = new Policy(policy);
238+
ns.contextStore = new ContextStore(contextStore);
236239
await Promise.allSettled(
237240
Object.entries(allSeen).map(
238241
async ([tabId, seen]) => {
@@ -336,6 +339,17 @@ var LifeCycle = (() => {
336339
if (changed) {
337340
await ns.savePolicy();
338341
}
342+
if (ns.contextStore) {
343+
changed = false;
344+
for (let k of Object.keys(ns.contextStore.policies)){
345+
for (let p of ns.contextStore.policies[k].getPresets(presetNames)) {
346+
if (callback(p)) changed = true;
347+
}
348+
}
349+
if (changed) {
350+
await ns.saveContextStore();
351+
}
352+
}
339353
};
340354

341355
const configureNewCap = async (cap, presetNames, capsFilter) => {

src/bg/RequestGuard.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,24 +381,27 @@
381381

382382
const wantsContext = checked.includes("ctx");
383383

384-
let { siteMatch, contextMatch, perms } = ns.policy.get(key, contextUrl);
384+
let cookieStoreId = sender.tab && sender.tab.cookieStoreId;
385+
let policy = ns.getPolicy(cookieStoreId);
386+
let { contextMatch, perms } = policy.get(key, contextUrl);
385387

386388
if (!perms.capabilities.has(policyType) ||
387389
!contextMatch && wantsContext && ctxKey) {
388390

389391
const wantsTemp = forcedTemp || checked.includes("temp");
390392
if (!contextMatch) {
391-
const isDefault = perms === ns.policy.DEFAULT;
393+
const isDefault = perms === policy.DEFAULT;
392394
perms = perms.clone();
393395
if (isDefault) perms.temp = wantsTemp;
394-
ns.policy.set(key, perms);
396+
policy.set(key, perms);
395397
if (ctxKey && wantsContext) {
396398
perms.contextual.set(ctxKey, perms = perms.clone(/* noContext = */ true));
397399
}
398400
}
399401
perms.temp = wantsTemp;
400402
perms.capabilities.add(policyType);
401403
await ns.savePolicy();
404+
await ns.saveContextStore();
402405
await RequestGuard.DNRPolicy?.update();
403406
}
404407
return {enable: key};
@@ -645,13 +648,14 @@
645648
function intersectCapabilities(policyMatch, request) {
646649
const {cascadePermissions, cascadeRestrictions} = ns.sync;
647650
if (request.frameId !== 0 && cascadeRestrictions || request.type != "main_frame" && cascadePermissions) {
648-
const {tabUrl, frameAncestors} = request;
651+
const {tabUrl, frameAncestors, cookieStoreId} = request;
649652
const topUrl = tabUrl ||
650653
cascadePermissions && request.frameId == 0 && request.documentUrl ||
651654
frameAncestors && frameAncestors[frameAncestors?.length - 1]?.url ||
652655
TabCache.get(request.tabId)?.url;
653656
if (topUrl) {
654-
return ns.policy.cascade(policyMatch, topUrl, {
657+
const policy = ns.getPolicy(cookieStoreId);
658+
return policy.cascade(policyMatch, topUrl, {
655659
permissions: cascadePermissions,
656660
restrictions: cascadeRestrictions,
657661
}).capabilities;
@@ -719,9 +723,10 @@
719723

720724
function checkLANRequest(request) {
721725
if (!ns.isEnforced(request.tabId)) return ALLOW;
722-
let {originUrl, url} = request;
726+
let {originUrl, url, cookieStoreId} = request;
727+
let policy = ns.getPolicy(cookieStoreId);
723728
if (originUrl && !Sites.isInternal(originUrl) && url.startsWith("http") &&
724-
!ns.policy.can(originUrl, "lan", ns.policyContext(request))) {
729+
!policy.can(originUrl, "lan", ns.policyContext(request))) {
725730
// we want to block any request whose origin resolves to at least one external WAN IP
726731
// and whose destination resolves to at least one LAN IP
727732
const {proxyInfo} = request; // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
@@ -756,9 +761,9 @@
756761

757762
normalizeRequest(request);
758763

759-
let {tabId, type, url, originUrl} = request;
764+
let {tabId, type, cookieStoreId, url, originUrl} = request;
760765

761-
const { policy } = ns;
766+
const policy = ns.getPolicy(cookieStoreId);
762767

763768
let previous = recent.find(request);
764769
if (previous) {
@@ -917,12 +922,12 @@
917922
let result = ALLOW;
918923

919924
pending.headersProcessed = true;
920-
let {url, tabId, responseHeaders, type} = request;
925+
let {url, tabId, cookieStoreId, responseHeaders, type} = request;
921926
let isMainFrame = type === "main_frame";
922927
try {
923928
let capabilities;
924929
if (ns.isEnforced(tabId)) {
925-
const { policy } = ns;
930+
const policy = ns.getPolicy(cookieStoreId);
926931
const policyMatch = policy.get(url, ns.policyContext(request));
927932
let { perms } = policyMatch;
928933
if (isMainFrame) {
@@ -1013,13 +1018,14 @@
10131018
async function injectPolicyScript(details) {
10141019
await ns.initializing;
10151020
if (ns.local.debug?.disablePolicyInjection) return ''; // DEV_ONLY
1016-
const {url, tabId, frameId, type} = details;
1021+
const {url, tabId, frameId, cookieStoreId, type} = details;
10171022
const isTop = type == "main_frame";
10181023
const domPolicy = await ns.computeChildPolicy(
10191024
{ url },
10201025
{
10211026
tab: { id: tabId, url: isTop ? url : null },
10221027
frameId: isTop ? 0 : frameId,
1028+
cookieStoreId,
10231029
}
10241030
);
10251031
domPolicy.navigationURL = url;

src/bg/Settings.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ var Settings = {
100100
async update(settings) {
101101
let {
102102
policy,
103+
contextStore,
103104
xssUserChoices,
104105
tabId,
105106
unrestrictedTab,
@@ -179,6 +180,7 @@ var Settings = {
179180
// User is resetting options:
180181
// pick either current Tor Browser Security Level or default NoScript policy
181182
policy = ns.local.torBrowserPolicy || this.createDefaultDryPolicy();
183+
contextStore = new ContextStore().dry();
182184
reloadOptionsUI = true;
183185
}
184186

@@ -198,6 +200,18 @@ var Settings = {
198200
await ns.savePolicy();
199201
}
200202

203+
if (contextStore) {
204+
ns.contextStore = new ContextStore(contextStore);
205+
}
206+
207+
if (policy && ns.contextStore) {
208+
ns.contextStore.updatePresets(ns.policy);
209+
}
210+
211+
if (contextStore || (policy && ns.contextStore)) {
212+
await ns.saveContextStore();
213+
}
214+
201215
if (typeof unrestrictedTab === "boolean") {
202216
await ns.toggleTabRestrictions(tabId, !unrestrictedTab);
203217
}
@@ -245,6 +259,7 @@ var Settings = {
245259
knownCapabilities: Permissions.ALL,
246260
},
247261
policy: ns.policy.dry(),
262+
contextStore: ns.contextStore.dry(),
248263
local: ns.local,
249264
sync: ns.sync,
250265
xssUserChoices: XSS.getUserChoices(),

src/bg/TabGuard.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,10 @@ var TabGuard = (() => {
184184

185185
// we suspect tabs which 1) have not been removed/discarded, 2) are restricted by policy, 3) can run JavaScript
186186
let suspiciousTabs = [...ties].map(TabCache.get).filter(
187-
tab => tab && !tab.discarded && ns.isEnforced(tab.id) &&
188-
(!(tab._isExplicitOrigin = tab._isExplicitOrigin || /^(?:https?|ftps?|file):/.test(tab.url)) || ns.policy.can(tab.url, "script"))
187+
tab => tab && !tab.discarded && ns.isEnforced(tab.id) && (
188+
!(tab._isExplicitOrigin = tab._isExplicitOrigin || /^(?:https?|ftps?|file):/.test(tab.url)) ||
189+
ns.getPolicy(tab.cookieStoreId).can(tab.url, "script")
190+
)
189191
);
190192

191193
return suspiciousTabs.length > 0 && (async () => {
@@ -222,7 +224,7 @@ var TabGuard = (() => {
222224
}
223225
if (tab.url !== "about:blank") {
224226
debug(`Real origin for ${tab._externalUrl} (tab ${tab.id}) is ${tab.url}.`);
225-
if (!ns.policy.can(tab.url, "script")) return;
227+
if (!ns.getPolicy(tab.cookieStoreId).can(tab.url, "script")) return;
226228
}
227229
}
228230
if (!tab._contentType) {

src/bg/main.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@
105105
}
106106
}
107107

108+
if (!ns.contextStore) { // it could have been already retrieved by LifeCycle
109+
const contextStoreData = (await Storage.get("sync", "contextStore")).contextStore;
110+
if (contextStoreData) {
111+
ns.contextStore = new ContextStore(contextStoreData);
112+
await ns.contextStore.updateContainers(ns.policy);
113+
} else {
114+
log("No container data found. Initializing new policies.")
115+
ns.contextStore = new ContextStore();
116+
await ns.contextStore.updateContainers(ns.policy);
117+
await ns.saveContextStore();
118+
}
119+
}
108120

109121
const {isTorBrowser} = ns.local;
110122
Sites.onionSecure = isTorBrowser;
@@ -177,10 +189,12 @@
177189
tabId = -1
178190
}) {
179191
const policy = ns.policy.dry(true);
192+
const contextStore = ns.contextStore.dry(true);
180193
const seen = tabId !== -1 ? await ns.collectSeen(tabId) : null;
181194
const xssUserChoices = await XSS.getUserChoices();
182195
await Messages.send("settings", {
183196
policy,
197+
contextStore,
184198
seen,
185199
xssUserChoices,
186200
local: ns.local,
@@ -281,6 +295,7 @@
281295
}
282296

283297
let _policy = null;
298+
let _contextStore = null;
284299

285300
globalThis.ns = {
286301
running: false,
@@ -289,6 +304,11 @@
289304
RequestGuard.DNRPolicy?.update();
290305
},
291306
get policy() { return _policy; },
307+
set contextStore(c) {
308+
_contextStore = c;
309+
RequestGuard.DNRPolicy?.update();
310+
},
311+
get contextStore() { return _contextStore; },
292312
local: null,
293313
sync: null,
294314
initializing: null,
@@ -319,12 +339,29 @@
319339
return tab?.url || documentUrl || url;
320340
},
321341
requestCan(request, capability) {
322-
return !this.isEnforced(request.tabId) || this.policy.can(request.url, capability, this.policyContext(request));
342+
return (
343+
!this.isEnforced(request.tabId) ||
344+
ns.getPolicy(request.cookieStoreId).can(request.url, capability, this.policyContext(request))
345+
);
346+
},
347+
348+
getPolicy(cookieStoreId){
349+
if (
350+
ns.contextStore &&
351+
ns.contextStore.enabled &&
352+
ns.contextStore.policies.hasOwnProperty(cookieStoreId)
353+
) {
354+
let currentPolicy = ns.contextStore.policies[cookieStoreId];
355+
debug("id", cookieStoreId, "has cookiestore", currentPolicy);
356+
if (currentPolicy) return currentPolicy;
357+
}
358+
debug("default cookiestore", cookieStoreId);
359+
return ns.policy;
323360
},
324361

325362
async computeChildPolicy({url, contextUrl}, sender) {
326363
await ns.initializing;
327-
let { tab, origin, frameId, documentLifecycle } = sender;
364+
let { tab, origin, frameId, cookieStoreId, documentLifecycle } = sender;
328365
const tabId = tab ? tab.id : -1;
329366

330367
if (url == sender.url) {
@@ -341,7 +378,8 @@
341378
}
342379
}
343380
}
344-
let policy = ns.policy;
381+
if (!cookieStoreId && tab) cookieStoreId = tab.cookieStoreId;
382+
let policy = ns.getPolicy(cookieStoreId);
345383
const {isTorBrowser} = ns.local;
346384
if (!policy) {
347385
console.log("Policy is null, initializing: %o, sending fallback.", ns.initializing);
@@ -449,6 +487,19 @@
449487
return this.policy;
450488
},
451489

490+
async saveContextStore() {
491+
if (this.contextStore) {
492+
await Promise.allSettled([
493+
Storage.set("sync", {
494+
contextStore: this.contextStore.dry()
495+
}),
496+
session.save(),
497+
browser.webRequest.handlerBehaviorChanged()
498+
]);
499+
}
500+
return this.contextStore;
501+
},
502+
452503
openOptionsPage({tab, focus, hilite}) {
453504
const url = new URL(browser.runtime.getManifest().options_ui.page);
454505
if (tab !== undefined) {
@@ -510,5 +561,7 @@ if (!browser.windows) {
510561
log("All tabs closed: revoking temporary permissions.");
511562
ns.policy.revokeTemp();
512563
ns.savePolicy();
564+
ns.contextStore.revokeTemp();
565+
ns.saveContextStore();
513566
});
514567
}

src/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"webRequestFilterResponse",
3737
"webRequestFilterResponse.serviceWorkerScript",
3838
"dns",
39-
"<all_urls>"
39+
"<all_urls>",
40+
"contextualIdentities"
4041
],
4142
"host_permissions": [
4243
"<all_urls>"
@@ -62,6 +63,7 @@
6263
"/nscl/common/Sites.js",
6364
"/nscl/common/Permissions.js",
6465
"/nscl/common/Policy.js",
66+
"/nscl/common/ContextStore.js",
6567
"/nscl/common/locale.js",
6668
"/nscl/common/Storage.js",
6769
"/nscl/common/include.js",

0 commit comments

Comments
 (0)