From 3ea1f719659b4a69c1a9203b45f72988c491c9f3 Mon Sep 17 00:00:00 2001 From: Aaron Kollasch Date: Sat, 6 Nov 2021 16:07:09 -0400 Subject: [PATCH] Add support for container tabs --- .gitmodules | 1 + src/bg/LifeCycle.js | 15 ++++- src/bg/RequestGuard.js | 30 +++++---- src/bg/Settings.js | 9 +++ src/bg/main.js | 53 ++++++++++++++- src/manifest.json | 4 +- src/nscl | 2 +- src/ui/options.css | 32 +++++++++ src/ui/options.html | 22 ++++++ src/ui/options.js | 150 ++++++++++++++++++++++++++++++++++++++--- src/ui/popup.css | 12 ++++ src/ui/popup.html | 1 + src/ui/popup.js | 20 +++++- src/ui/ui.js | 92 +++++++++++++++++-------- 14 files changed, 386 insertions(+), 57 deletions(-) diff --git a/.gitmodules b/.gitmodules index c93b31b8..0185a08d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "nscl"] path = src/nscl url = ../nscl.git + branch = container-tabs diff --git a/src/bg/LifeCycle.js b/src/bg/LifeCycle.js index 835d961f..22828646 100644 --- a/src/bg/LifeCycle.js +++ b/src/bg/LifeCycle.js @@ -108,6 +108,7 @@ var LifeCycle = (() => { let {url} = tab; let {cypherText, key, iv} = await encrypt(JSON.stringify({ policy: ns.policy.dry(true), + contextStore: ns.contextStore.dry(true), allSeen, unrestrictedTabs: [...ns.unrestrictedTabs] })); @@ -208,7 +209,7 @@ var LifeCycle = (() => { iv }, key, cypherText ); - let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded)); + let {policy, contextStore, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded)); if (!policy) { throw new error("Ephemeral policy not found in survival tab %s!", tabId); } @@ -216,6 +217,7 @@ var LifeCycle = (() => { destroyIfNeeded(); if (ns.initializing) await ns.initializing; ns.policy = new Policy(policy); + ns.contextStore = new ContextStore(contextStore); await Promise.allSettled( Object.entries(allSeen).map( async ([tabId, seen]) => { @@ -308,6 +310,17 @@ var LifeCycle = (() => { if (changed) { await ns.savePolicy(); } + if (ns.contextStore) { + changed = false; + for (let k of Object.keys(ns.contextStore.policies)){ + for (let p of ns.contextStore.policies[k].getPresets(presetNames)) { + if (callback(p)) changed = true; + } + } + if (changed) { + await ns.saveContextStore(); + } + } }; const configureNewCap = async (cap, presetNames, capsFilter) => { diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index fe66d0b3..57fcd02b 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -379,7 +379,9 @@ const wantsContext = checked.includes("ctx"); - let { siteMatch, contextMatch, perms } = ns.policy.get(key, contextUrl); + let cookieStoreId = sender.tab && sender.tab.cookieStoreId; + let policy = ns.getPolicy(cookieStoreId); + let { siteMatch, contextMatch, perms } = policy.get(key, contextUrl); if (!perms.capabilities.has(policyType) || !contextMatch && wantsContext && ctxKey) { @@ -389,7 +391,7 @@ const isDefault = perms === ns.policy.DEFAULT; perms = perms.clone(); if (isDefault) perms.temp = wantsTemp; - ns.policy.set(key, perms); + policy.set(key, perms); if (ctxKey && wantsContext) { perms.contextual.set(ctxKey, perms = perms.clone(/* noContext = */ true)); } @@ -397,6 +399,7 @@ perms.temp = wantsTemp; perms.capabilities.add(policyType); await ns.savePolicy(); + await ns.saveContextStore(); await RequestGuard.DNRPolicy?.update(); } return {enable: key}; @@ -638,12 +641,13 @@ function intersectCapabilities(policyMatch, request) { if (request.frameId !== 0 && ns.sync.cascadeRestrictions) { - const {tabUrl, frameAncestors} = request; + const {tabUrl, frameAncestors, cookieStoreId} = request; const topUrl = tabUrl || frameAncestors && frameAncestors[frameAncestors?.length - 1]?.url || TabCache.get(request.tabId)?.url; if (topUrl) { - return ns.policy.cascadeRestrictions(policyMatch, topUrl).capabilities; + const policy = ns.getPolicy(cookieStoreId); + return policy.cascadeRestrictions(policyMatch, topUrl).capabilities; } } return policyMatch.perms.capabilities; @@ -708,9 +712,10 @@ function checkLANRequest(request) { if (!ns.isEnforced(request.tabId)) return ALLOW; - let {originUrl, url} = request; + let {originUrl, url, cookieStoreId} = request; + let policy = ns.getPolicy(cookieStoreId); if (originUrl && !Sites.isInternal(originUrl) && url.startsWith("http") && - !ns.policy.can(originUrl, "lan", ns.policyContext(request))) { + !policy.can(originUrl, "lan", ns.policyContext(request))) { // we want to block any request whose origin resolves to at least one external WAN IP // and whose destination resolves to at least one LAN IP const {proxyInfo} = request; // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo @@ -745,9 +750,9 @@ normalizeRequest(request); - let {tabId, type, url, originUrl} = request; + let {tabId, type, cookieStoreId, url, originUrl} = request; - const {policy} = ns + const policy = ns.getPolicy(cookieStoreId); let previous = recent.find(request); if (previous) { @@ -907,12 +912,12 @@ let promises = []; pending.headersProcessed = true; - let {url, documentUrl, tabId, responseHeaders, type} = request; + let {url, documentUrl, tabId, cookieStoreId, responseHeaders, type} = request; let isMainFrame = type === "main_frame"; try { let capabilities; if (ns.isEnforced(tabId)) { - const { policy } = ns; + const policy = ns.getPolicy(cookieStoreId); const policyMatch = policy.get(url, ns.policyContext(request)); let { perms } = policyMatch; if (isMainFrame) { @@ -1008,13 +1013,14 @@ async function injectPolicyScript(details) { await ns.initializing; if (ns.local.debug?.disablePolicyInjection) return ''; // DEV_ONLY - const {url, tabId, frameId, type} = details; + const {url, tabId, frameId, cookieStoreId, type} = details; const isTop = type == "main_frame"; const domPolicy = await ns.computeChildPolicy( { url }, { tab: { id: tabId, url: isTop ? url : null }, frameId: isTop ? 0 : frameId, + cookieStoreId, } ); domPolicy.navigationURL = url; @@ -1106,4 +1112,4 @@ }, filterDocs, ["blocking", "responseHeaders"])).install(); } } -} \ No newline at end of file +} diff --git a/src/bg/Settings.js b/src/bg/Settings.js index aa01f0e3..31165c30 100644 --- a/src/bg/Settings.js +++ b/src/bg/Settings.js @@ -100,6 +100,7 @@ var Settings = { async update(settings) { let { policy, + contextStore, xssUserChoices, tabId, unrestrictedTab, @@ -176,6 +177,7 @@ var Settings = { // User is resetting options: // pick either current Tor Browser Security Level or default NoScript policy policy = ns.local.torBrowserPolicy || this.createDefaultDryPolicy(); + contextStore = new ContextStore().dry(); reloadOptionsUI = true; } @@ -195,6 +197,12 @@ var Settings = { await ns.savePolicy(); } + if (contextStore) { + let newContextStore = new ContextStore(contextStore); + ns.contextStore = newContextStore + await ns.saveContextStore(); + } + if (typeof unrestrictedTab === "boolean") { await ns.toggleTabRestrictions(tabId, !unrestrictedTab); } @@ -242,6 +250,7 @@ var Settings = { knownCapabilities: Permissions.ALL, }, policy: ns.policy.dry(), + contextStore: ns.contextStore.dry(), local: ns.local, sync: ns.sync, xssUserChoices: XSS.getUserChoices(), diff --git a/src/bg/main.js b/src/bg/main.js index 82f01fdf..7c0e00a7 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -105,6 +105,19 @@ } } + if (!ns.contextStore) { // it could have been already retrieved by LifeCycle + const contextStoreData = (await Storage.get("sync", "contextStore")).contextStore; + if (contextStoreData) { + ns.contextStore = new ContextStore(contextStoreData); + await ns.contextStore.updateContainers(ns.policy); + } else { + log("No container data found. Initializing new policies.") + ns.contextStore = new ContextStore(); + await ns.contextStore.updateContainers(ns.policy); + await ns.saveContextStore(); + } + } + const {isTorBrowser} = ns.local; Sites.onionSecure = isTorBrowser; @@ -175,10 +188,12 @@ tabId = -1 }) { const policy = ns.policy.dry(true); + const contextStore = ns.contextStore.dry(true); const seen = tabId !== -1 ? await ns.collectSeen(tabId) : null; const xssUserChoices = await XSS.getUserChoices(); await Messages.send("settings", { policy, + contextStore, seen, xssUserChoices, local: ns.local, @@ -260,6 +275,7 @@ } let _policy = null; + let _contextStore = null; globalThis.ns = { running: false, @@ -268,6 +284,11 @@ RequestGuard.DNRPolicy?.update(); }, get policy() { return _policy; }, + set contextStore(c) { + _contextStore = c; + RequestGuard.DNRPolicy?.update(); + }, + get contextStore() { return _contextStore; }, local: null, sync: null, initializing: null, @@ -301,13 +322,28 @@ return !this.isEnforced(request.tabId) || this.policy.can(request.url, capability, this.policyContext(request)); }, + getPolicy(cookieStoreId){ + if ( + ns.contextStore && + ns.contextStore.enabled && + ns.contextStore.policies.hasOwnProperty(cookieStoreId) + ) { + let currentPolicy = ns.contextStore.policies[cookieStoreId]; + debug("id", cookieStoreId, "has cookiestore", currentPolicy); + if (currentPolicy) return currentPolicy; + } + debug("default cookiestore", cookieStoreId); + return ns.policy; + }, + async computeChildPolicy({url, contextUrl}, sender) { await ns.initializing; - let {tab, origin, frameId, documentLifecycle} = sender; + let {tab, origin, frameId, cookieStoreId, documentLifecycle} = sender; if (url == sender.url && url == "about:blank") { url = origin; } - let policy = ns.policy; + if (!cookieStoreId && tab) cookieStoreId = tab.cookieStoreId; + let policy = ns.getPolicy(cookieStoreId); const {isTorBrowser} = ns.local; if (!policy) { console.log("Policy is null, initializing: %o, sending fallback.", ns.initializing); @@ -414,6 +450,19 @@ return this.policy; }, + async saveContextStore() { + if (this.contextStore) { + await Promise.allSettled([ + Storage.set("sync", { + policy: this.contextStore.dry() + }), + session.save(), + browser.webRequest.handlerBehaviorChanged() + ]); + } + return this.contextStore; + }, + openOptionsPage({tab, focus, hilite}) { const url = new URL(browser.runtime.getManifest().options_ui.page); if (tab !== undefined) { diff --git a/src/manifest.json b/src/manifest.json index 532a1b76..6785504c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -36,7 +36,8 @@ "webRequestFilterResponse", "webRequestFilterResponse.serviceWorkerScript", "dns", - "" + "", + "contextualIdentities" ], "host_permissions": [ "" @@ -62,6 +63,7 @@ "/nscl/common/Sites.js", "/nscl/common/Permissions.js", "/nscl/common/Policy.js", + "/nscl/common/ContextStore.js", "/nscl/common/locale.js", "/nscl/common/Storage.js", "/nscl/common/include.js", diff --git a/src/nscl b/src/nscl index 0e847ec7..9a3c880a 160000 --- a/src/nscl +++ b/src/nscl @@ -1 +1 @@ -Subproject commit 0e847ec7218558ca7c51dcfcc5040de2504f1174 +Subproject commit 9a3c880aa54be6c8c921da48af543fe77dfb9eea diff --git a/src/ui/options.css b/src/ui/options.css index 2486affb..b621d729 100644 --- a/src/ui/options.css +++ b/src/ui/options.css @@ -86,6 +86,24 @@ fieldset:disabled { flex: 2 2; } +.per-site-buttons { + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + width: 100%; + text-align: right; + margin: .5em 0 0 0; +} +#btn-clear-container { + margin-inline-start: .5em; +} +#copy-container { + margin-inline: .5em; +} +#copy-container-label { + margin-block: auto; +} + #policy { display: block; margin-top: .5em; @@ -95,6 +113,12 @@ fieldset:disabled { .hide, body:not(.debug) div.debug { display: none; } +#context-store { + display: block; + margin-top: .5em; + min-height: 20em; + width: 90%; +} #debug-tools { padding-left: 2.5em; @@ -114,6 +138,14 @@ fieldset:disabled { font-weight: bold; } +#context-store-error { + background: red; + color: #ff8; + padding: 0; + margin: 0; + font-weight: bold; +} + input, button { font-size: 1em; } diff --git a/src/ui/options.html b/src/ui/options.html index 318251d3..f010d993 100644 --- a/src/ui/options.html +++ b/src/ui/options.html @@ -53,6 +53,9 @@

+ +
+
+ + +
+ +
@@ -204,6 +220,12 @@

diff --git a/src/ui/popup.js b/src/ui/popup.js index 3ac71d33..8de5eb6d 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -87,6 +87,7 @@ addEventListener("unload", e => { } else { tabId = tab.id; } + let cookieStoreId = pageTab.cookieStoreId; addEventListener("keydown", e => { if (e.code === "Enter") { @@ -116,6 +117,19 @@ addEventListener("unload", e => { }); } + if (UI.contextStore && UI.contextStore.enabled && browser.contextualIdentities) { + try { + let containerName = (await browser.contextualIdentities.get(cookieStoreId)).name; + document.querySelector("#container-id").textContent = containerName; + debug("found container name", containerName, "for cookieStoreId", cookieStoreId); + } catch(err) { + document.querySelector("#container-id").textContent = "Default"; + debug("no container for cookieStoreId", cookieStoreId, "error:", err.message); + } + } else { + document.querySelector("#container-id").style.visibility = 'hidden'; + } + await include("/ui/toolbar.js"); UI.toolbarInit(); { @@ -315,7 +329,9 @@ addEventListener("unload", e => { let justDomains = !UI.local.showFullAddresses; - sitesUI = new UI.Sites(document.getElementById("sites")); + let policy = await UI.getPolicy(cookieStoreId); + debug("popup policy", policy); + sitesUI = new UI.Sites(document.getElementById("sites"), UI.DEF_PRESETS, policy); sitesUI.onChange = (row) => { const reload = sitesUI.anyPermissionsChanged() @@ -352,7 +368,7 @@ addEventListener("unload", e => { typesMap } = sitesUI; typesMap.clear(); - let policySites = UI.policy.sites; + let policySites = policy.sites; let domains = new Map(); let protocols = new Set(); function urlToLabel(url) { diff --git a/src/ui/ui.js b/src/ui/ui.js index c6fc2897..1e8b0de9 100644 --- a/src/ui/ui.js +++ b/src/ui/ui.js @@ -44,6 +44,7 @@ var UI = (() => { "/nscl/common/Sites.js", "/nscl/common/Permissions.js", "/nscl/common/Policy.js", + "/nscl/common/ContextStore.js" ]; this.mobile = UA.mobile; @@ -55,7 +56,8 @@ var UI = (() => { async settings(m) { if (UI.tabId !== m.tabId) return; UI.policy = new Policy(m.policy); - UI.snapshot = UI.policy.snapshot; + UI.contextStore = new ContextStore(m.contextStore); + UI.snapshot = UI.policy.snapshot+UI.contextStore.snapshot; UI.seen = m.seen; UI.tabLess = m.tabLess; UI.unrestrictedTab = m.unrestrictedTab; @@ -97,7 +99,7 @@ var UI = (() => { await inited; this.initialized = true; - debug("Imported", Policy); + debug("Imported", Policy, ContextStore); }, async pullSettings() { try { @@ -107,10 +109,12 @@ var UI = (() => { browser.runtime.reload(); } }, - async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected, command}) { + async updateSettings({policy, contextStore, xssUserChoices, unrestrictedTab, local, sync, reloadAffected, command}) { if (policy) policy = policy.dry(true); + if (contextStore) contextStore = contextStore.dry(true); return await Messages.send("updateSettings", { policy, + contextStore, xssUserChoices, unrestrictedTab, local, @@ -130,13 +134,41 @@ var UI = (() => { async revokeTemp(reloadAffected = false) { this.policy.revokeTemp(); + this.contextStore.revokeTemp(); if (this.isDirty(true)) { - await this.updateSettings({policy: this.policy, reloadAffected}); + await this.updateSettings({policy: this.policy, contextStore: this.contextStore, reloadAffected}); + await this.updateSettings({policy, contextStore, reloadAffected}); + } + }, + + async getPolicy(cookieStoreId) { + await this.contextStore.updateContainers(this.policy); + if (this.contextStore.enabled && this.contextStore.policies.hasOwnProperty(cookieStoreId)) { + let currentPolicy = this.contextStore.policies[cookieStoreId]; + debug("id", cookieStoreId, "has cookiestore", currentPolicy); + return currentPolicy; + } else { + debug("default cookiestore", cookieStoreId); + return this.policy; + } + }, + + async replacePolicy(cookieStoreId, policy) { + await this.contextStore.updateContainers(this.policy); + if (this.contextStore.policies.hasOwnProperty(cookieStoreId)) { + this.contextStore.policies[cookieStoreId] = policy; + debug("replaced id", cookieStoreId, "with policy", policy); + let currentPolicy = this.contextStore.policies[cookieStoreId]; + return currentPolicy; + } else { + this.policy = policy; + debug("replaced default cookiestore", cookieStoreId, "with policy", policy); + return this.policy; } }, isDirty(reset = false) { - let currentSnapshot = this.policy.snapshot; + let currentSnapshot = this.policy.snapshot+this.contextStore.snapshot; let dirty = currentSnapshot != this.snapshot; if (reset) this.snapshot = currentSnapshot; return dirty; @@ -322,7 +354,7 @@ var UI = (() => { function fireOnChange(sitesUI, data) { if (UI.isDirty(true)) { - UI.updateSettings({policy: UI.policy}); + UI.updateSettings({policy: UI.policy, contextStore: UI.contextStore}); if (sitesUI.onChange) sitesUI.onChange(data, this); } } @@ -401,10 +433,11 @@ var UI = (() => { const EXTRA_CAPS = ["x-load"]; UI.Sites = class { - constructor(parentNode, presets = DEF_PRESETS) { + constructor(parentNode, presets = DEF_PRESETS, policy = null) { this.parentNode = parentNode; + this.policy = (policy)? policy : UI.policy; this.uiCount = UI.Sites.count = (UI.Sites.count || 0) + 1; - this.sites = UI.policy.sites; + this.sites = this.policy.sites; this.presets = presets; this.customizing = null; this.typesMap = new Map(); @@ -543,6 +576,11 @@ var UI = (() => { this.customize(null); this.sitesCount = 0; + this.sites = ({ + trusted: [], + untrusted: [], + custom: {}, + }) } siteNeeds(site, type) { @@ -589,7 +627,6 @@ var UI = (() => { let tempToggle = preset.parentNode.querySelector("input.temp"); if (ev.type === "change") { - let { policy } = UI; if (!row._originalPerms) { row._originalPerms = row.perms.clone(); Object.defineProperty(row, "permissionsChanged", { @@ -603,7 +640,7 @@ var UI = (() => { if (!opt) return; let context = opt.value; if (context === "*") context = null; - ({ siteMatch, perms, contextMatch } = policy.get(siteMatch, context)); + ({ siteMatch, perms, contextMatch } = this.policy.get(siteMatch, context)); if (!context) { row._customPerms = perms; } else if (contextMatch !== context) { @@ -622,8 +659,8 @@ var UI = (() => { let presetValue = preset.value; let policyPreset = presetValue.startsWith("T_") - ? policy[presetValue.substring(2)].tempTwin - : policy[presetValue]; + ? this.policy[presetValue.substring(2)].tempTwin + : this.policy[presetValue]; if (policyPreset && row.perms !== policyPreset) { row.perms = policyPreset; @@ -641,7 +678,7 @@ var UI = (() => { row.perms = policyPreset; delete row._customPerms; if (siteMatch) { - policy.set(siteMatch, policyPreset); + this.policy.set(siteMatch, policyPreset); } else { this.customize(policyPreset, preset, row); } @@ -658,7 +695,7 @@ var UI = (() => { temp )); row.perms = perms; - policy.set(siteMatch, perms); + this.policy.set(siteMatch, perms); this.customize(perms, preset, row); } } @@ -775,7 +812,7 @@ var UI = (() => { let selected = ctxSelect.querySelector("option:checked"); ctxReset.disabled = !(selected?.value !== "*"); ctxReset.onclick = () => { - let perms = UI.policy.get(row.siteMatch).perms; + let perms = this.policy.get(row.siteMatch).perms; perms.contextual.delete(row.contextMatch); fireOnChange(this, row); selected.previousElementSibling.selected = true; @@ -962,7 +999,7 @@ var UI = (() => { site = site.site; context = site.context; } - let { siteMatch, perms, contextMatch } = UI.policy.get(site, context); + let { siteMatch, perms, contextMatch } = this.policy.get(site, context); this.append(site, siteMatch, perms, contextMatch); if (!hasTemp) hasTemp = perms.temp; } @@ -1016,16 +1053,19 @@ var UI = (() => { } async tempTrustAll() { - let { policy } = UI; let changed = 0; for (let row of this.allSiteRows()) { if (row._preset === "DEFAULT") { - policy.set(row._site, policy.TRUSTED.tempTwin); + this.policy.set(row._site, this.policy.TRUSTED.tempTwin); changed++; } } if (changed && UI.isDirty(true)) { - await UI.updateSettings({ policy, reloadAffected: true }); + await UI.updateSettings({ + policy: UI.policy, + contextStore: UI.contextStore, + reloadAffected: true + }); } return changed; } @@ -1064,7 +1104,6 @@ var UI = (() => { contextMatch, perms ); - const { policy } = UI; let row = this.rowTemplate.cloneNode(true); row.sitesCount = sitesCount; @@ -1072,7 +1111,7 @@ var UI = (() => { try { url = new URL(site); if (siteMatch !== site && siteMatch === url.protocol) { - perms = policy.DEFAULT; + perms = this.policy.DEFAULT; } } catch (e) { if (/^(\w+:)\/*$/.test(site)) { @@ -1095,7 +1134,7 @@ var UI = (() => { let { hostname } = url; let overrideDefault = site && url.protocol && site !== url.protocol - ? policy.get(url.protocol, contextMatch) + ? this.policy.get(url.protocol, contextMatch) : null; if (!overrideDefault?.siteMatch) overrideDefault = null; @@ -1175,7 +1214,7 @@ var UI = (() => { let getPresetName = (perms) => { let presetName = "CUSTOM"; for (let p of ["TRUSTED", "UNTRUSTED", "DEFAULT"]) { - let preset = policy[p]; + let preset = this.policy[p]; switch (perms) { case preset: presetName = p; @@ -1228,7 +1267,7 @@ var UI = (() => { row .querySelector(`.presets input[value="${p}"]`) .parentNode.querySelector("input.temp").checked = true; - perms = policy.TRUSTED.tempTwin; + perms = this.policy.TRUSTED.tempTwin; } } } @@ -1264,9 +1303,8 @@ var UI = (() => { if (site !== row.siteMatch) { this.customize(null); let focused = document.activeElement; - let { policy } = UI; - policy.set(row.siteMatch, policy.DEFAULT); - policy.set(site, row.perms); + this.policy.set(row.siteMatch, policy.DEFAULT); + this.policy.set(site, row.perms); for (let r of this.allSiteRows()) { if ( r !== row &&