tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit a868b40e6a70e5e5232aecaa3cebd49c6cd48824
parent 28f62ab3bb114cf1efb1ab41a2f48c6321e2e8a4
Author: kpatenio <kpatenio@mozilla.com>
Date:   Wed, 15 Oct 2025 02:24:03 +0000

Bug 1992299 — add support for site inclusions in IPPExceptionsManager. r=ip-protection-reviewers,fchasen

Differential Revision: https://phabricator.services.mozilla.com/D267504

Diffstat:
Mbrowser/app/profile/firefox.js | 1+
Mbrowser/components/ipprotection/IPPExceptionsManager.sys.mjs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mbrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js | 13+++++++++++++
Abrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/tests/xpcshell/xpcshell.toml | 2++
5 files changed, 286 insertions(+), 22 deletions(-)

diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -3477,6 +3477,7 @@ pref("browser.ipProtection.userEnabled", false); pref("browser.ipProtection.variant", ""); pref("browser.ipProtection.exceptionsMode", "all"); pref("browser.ipProtection.domainExclusions", ""); +pref("browser.ipProtection.domainInclusions", ""); pref("browser.ipProtection.log", false); pref("browser.ipProtection.guardian.endpoint", "https://vpn.mozilla.org/"); diff --git a/browser/components/ipprotection/IPPExceptionsManager.sys.mjs b/browser/components/ipprotection/IPPExceptionsManager.sys.mjs @@ -8,6 +8,7 @@ const lazy = {}; const MODE_PREF = "browser.ipProtection.exceptionsMode"; const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions"; +const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions"; const LOG_PREF = "browser.ipProtection.log"; const MODE = { @@ -28,6 +29,7 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { class ExceptionsManager { #inited = false; #exclusions = null; + #inclusions = null; #mode = MODE.ALL; /** @@ -47,6 +49,22 @@ class ExceptionsManager { } /** + * The set of domains to include for VPN protection. + * + * @returns {Set<string>} + * A set of domain names as strings. + * + * @example + * Set { "https://www.example.com", "https://www.bbc.co.uk" } + */ + get inclusions() { + if (!this.#inclusions || !(this.#inclusions instanceof Set)) { + this.#inclusions = new Set(); + } + return this.#inclusions; + } + + /** * The type of site exceptions for VPN. * * @see MODE @@ -61,7 +79,12 @@ class ExceptionsManager { } this.#mode = this.#getModePref(); - this.#loadExceptions(); + + this.#exclusions = new Set(); + this.#inclusions = new Set(); + this.#loadExceptionsForPref(EXCLUSIONS_PREF); + this.#loadExceptionsForPref(INCLUSIONS_PREF); + this.#inited = true; } @@ -129,6 +152,11 @@ class ExceptionsManager { if (pref === EXCLUSIONS_PREF) { prefString = this.domainExclusions; + } else if (pref === INCLUSIONS_PREF) { + prefString = this.domainInclusions; + } else { + lazy.logConsole.error(`Invalid pref ${pref} found.`); + return ""; } if (typeof prefString !== "string") { @@ -140,21 +168,19 @@ class ExceptionsManager { } /** - * If mode is MODE.ALL, initializes the exclusions set with domains from - * browser.ipProtection.domainExclusions. + * Initializes the exclusions set with domains from + * browser.ipProtection.domainExclusions, or + * initializes the inclusions set with domains from + * browser.ipProtection.domainInclusions. * - * @see MODE + * @param {string} pref + * The exclusions pref (browser.ipProtection.domainExclusions) + * or inclusions pref (browser.ipProtection.domainInclusions). * @see exclusions + * @see inclusions */ - #loadExceptions() { - if (this.#mode == MODE.ALL) { - this.#loadExclusions(); - } - } - - #loadExclusions() { - this.#exclusions = new Set(); - let prefString = this.#getExceptionPref(EXCLUSIONS_PREF); + #loadExceptionsForPref(pref) { + let prefString = this.#getExceptionPref(pref); if (!prefString) { return; @@ -163,25 +189,30 @@ class ExceptionsManager { let domains = prefString.trim().split(","); for (let domain of domains) { - if (!this.#canExcludeDomain(domain)) { + if (!this.#canCreateURI(domain)) { continue; } let uri = Services.io.newURI(domain); - this.#exclusions.add(uri.prePath); + + if (pref === EXCLUSIONS_PREF) { + this.#exclusions.add(uri.prePath); + } else if (pref === INCLUSIONS_PREF) { + this.#inclusions.add(uri.prePath); + } } } /** - * Checks if we can exclude a domain from VPN usage. + * Checks if we can add a domain as an exclusion or inclusion for VPN usage. * * @param {string} domain * The domain name. * @returns {boolean} - * True if we can exclude the domain because it meets our exclusion rules. + * True if a URI can be created from the domain string. * Else false. */ - #canExcludeDomain(domain) { + #canCreateURI(domain) { try { return !!Services.io.newURI(domain); } catch (e) { @@ -201,13 +232,15 @@ class ExceptionsManager { */ addException(domain) { // TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972) - if (this.#mode == MODE.ALL) { + if (this.#mode === MODE.ALL) { this.#addExclusion(domain); + } else if (this.#mode === MODE.SELECT) { + this.#addInclusion(domain); } } #addExclusion(domain) { - if (!this.#canExcludeDomain(domain)) { + if (!this.#canCreateURI(domain)) { return; } @@ -215,6 +248,15 @@ class ExceptionsManager { this.#updateExclusionPref(); } + #addInclusion(domain) { + if (!this.#canCreateURI(domain)) { + return; + } + + this.#inclusions.add(domain); + this.#updateInclusionPref(); + } + /** * If mode is MODE.ALL, removes a domain from the exclusions set. * @@ -226,8 +268,10 @@ class ExceptionsManager { */ removeException(domain) { // TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972) - if (this.#mode == MODE.ALL) { + if (this.#mode === MODE.ALL) { this.#removeExclusion(domain); + } else if (this.#mode === MODE.SELECT) { + this.#removeInclusion(domain); } } @@ -237,6 +281,12 @@ class ExceptionsManager { } } + #removeInclusion(domain) { + if (this.#inclusions.delete(domain)) { + this.#updateInclusionPref(); + } + } + /** * Updates the value of browser.ipProtection.domainExclusions * according to the latest version of the exclusions set. @@ -247,11 +297,20 @@ class ExceptionsManager { } /** + * Updates the value of browser.ipProtection.domainInclusions + * according to the latest version of the inclusions set. + */ + #updateInclusionPref() { + let newPrefString = [...this.#inclusions].join(","); + Services.prefs.setStringPref(INCLUSIONS_PREF, newPrefString); + } + + /** * Clear the exclusions set. */ #unloadExceptions() { - // TODO: clear inclusions set here too this.#exclusions = null; + this.#inclusions = null; } } @@ -266,6 +325,13 @@ XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter( IPPExceptionsManager, + "domainInclusions", + INCLUSIONS_PREF, + "" +); + +XPCOMUtils.defineLazyPreferenceGetter( + IPPExceptionsManager, "exceptionsMode", MODE_PREF, MODE.ALL diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js @@ -7,14 +7,18 @@ const { IPPExceptionsManager } = ChromeUtils.importESModule( "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" ); +const MODE_PREF = "browser.ipProtection.exceptionsMode"; const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions"; +const ALL_MODE = "all"; + /** * Tests manager initialization when there are no exclusions. */ add_task(async function test_IPPExceptionsManager_init_with_no_exclusions() { const stringPref = ""; + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref); IPPExceptionsManager.init(); @@ -29,6 +33,7 @@ add_task(async function test_IPPExceptionsManager_init_with_no_exclusions() { Assert.ok(!newStringPref, "String pref should be empty"); + Services.prefs.clearUserPref(MODE_PREF); Services.prefs.clearUserPref(EXCLUSIONS_PREF); IPPExceptionsManager.uninit(); @@ -43,6 +48,7 @@ add_task(async function test_IPPExceptionsManager_init_with_exclusions() { const site3 = "https://www.another.example.ca"; const stringPref = `${site1},${site2},${site3}`; + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref); IPPExceptionsManager.init(); @@ -73,6 +79,7 @@ add_task(async function test_IPPExceptionsManager_init_with_exclusions() { Assert.ok(newStringPref.includes(site2), `String pref includes ${site2}`); Assert.ok(newStringPref.includes(site3), `String pref includes ${site3}`); + Services.prefs.clearUserPref(MODE_PREF); Services.prefs.clearUserPref(EXCLUSIONS_PREF); IPPExceptionsManager.uninit(); }); @@ -84,6 +91,7 @@ add_task( async function test_IPPExceptionsManager_init_with_invalid_exclusions() { const invalidStringPref = "noturl"; + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); Services.prefs.setStringPref(EXCLUSIONS_PREF, invalidStringPref); IPPExceptionsManager.init(); @@ -94,6 +102,7 @@ add_task( "exclusions set should have 0 valid domains" ); + Services.prefs.clearUserPref(MODE_PREF); Services.prefs.clearUserPref(EXCLUSIONS_PREF); IPPExceptionsManager.uninit(); } @@ -105,6 +114,7 @@ add_task( add_task(async function test_IPPExceptionsManager_add_exclusions() { const site1 = "https://www.example.com"; + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); Services.prefs.setStringPref(EXCLUSIONS_PREF, site1); IPPExceptionsManager.init(); @@ -144,6 +154,7 @@ add_task(async function test_IPPExceptionsManager_add_exclusions() { `String pref includes ${validSite}` ); + Services.prefs.clearUserPref(MODE_PREF); Services.prefs.clearUserPref(EXCLUSIONS_PREF); IPPExceptionsManager.uninit(); }); @@ -154,6 +165,7 @@ add_task(async function test_IPPExceptionsManager_add_exclusions() { add_task(async function test_IPPExceptionsManager_remove_exclusions() { const site1 = "https://www.example.com"; + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); Services.prefs.setStringPref(EXCLUSIONS_PREF, site1); IPPExceptionsManager.init(); @@ -174,6 +186,7 @@ add_task(async function test_IPPExceptionsManager_remove_exclusions() { Assert.ok(!newStringPref, "String pref should be empty"); + Services.prefs.clearUserPref(MODE_PREF); Services.prefs.clearUserPref(EXCLUSIONS_PREF); IPPExceptionsManager.uninit(); }); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { IPPExceptionsManager } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" +); + +const MODE_PREF = "browser.ipProtection.exceptionsMode"; +const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions"; + +const SELECT_MODE = "select"; + +/** + * Tests manager initialization when there are no inclusions. + */ +add_task(async function test_IPPExceptionsManager_init_with_no_inclusions() { + const stringPref = ""; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref); + + IPPExceptionsManager.init(); + + Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); + Assert.ok( + !IPPExceptionsManager.inclusions.size, + "inclusions set should be empty" + ); + + Services.prefs.clearUserPref(MODE_PREF); + Services.prefs.clearUserPref(INCLUSIONS_PREF); + + IPPExceptionsManager.uninit(); +}); + +/** + * Tests the manager initialization with registered inclusions. + */ +add_task(async function test_IPPExceptionsManager_init_with_inclusions() { + const site1 = "https://www.example.com"; + const site2 = "https://www.example.org"; + const site3 = "https://www.another.example.ca"; + const stringPref = `${site1},${site2},${site3}`; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref); + + IPPExceptionsManager.init(); + + Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); + Assert.equal( + IPPExceptionsManager.inclusions.size, + 3, + "inclusions set should have 3 domains" + ); + + Assert.ok( + IPPExceptionsManager.inclusions.has(site1), + `inclusions set should include ${site1}` + ); + Assert.ok( + IPPExceptionsManager.inclusions.has(site2), + `inclusions set should include ${site2}` + ); + Assert.ok( + IPPExceptionsManager.inclusions.has(site3), + `inclusions set should include ${site3}` + ); + + Services.prefs.clearUserPref(MODE_PREF); + Services.prefs.clearUserPref(INCLUSIONS_PREF); + IPPExceptionsManager.uninit(); +}); + +/** + * Tests the manager initialization with an invalid pref string for inclusions. + */ +add_task( + async function test_IPPExceptionsManager_init_with_invalid_inclusions() { + const invalidStringPref = "noturl"; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + Services.prefs.setStringPref(INCLUSIONS_PREF, invalidStringPref); + + IPPExceptionsManager.init(); + + Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); + Assert.ok( + !IPPExceptionsManager.inclusions.size, + "inclusions set should have 0 valid domains" + ); + + Services.prefs.clearUserPref(MODE_PREF); + Services.prefs.clearUserPref(INCLUSIONS_PREF); + IPPExceptionsManager.uninit(); + } +); + +/** + * Tests that we can add valid domains to the inclusions set. + */ +add_task(async function test_IPPExceptionsManager_add_inclusions() { + const site1 = "https://www.example.com"; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + Services.prefs.setStringPref(INCLUSIONS_PREF, site1); + + IPPExceptionsManager.init(); + + Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); + + const validSite = "https://www.another.example.com"; + const dupeSite = site1; + const invalidSite = "noturl"; + + IPPExceptionsManager.addException(validSite); + IPPExceptionsManager.addException(dupeSite); + IPPExceptionsManager.addException(invalidSite); + IPPExceptionsManager.addException(null); + IPPExceptionsManager.addException(undefined); + + Assert.equal( + IPPExceptionsManager.inclusions.size, + 2, + "inclusions set should only have 2 domains" + ); + + Assert.ok( + IPPExceptionsManager.inclusions.has(site1), + `inclusions set should include ${site1}` + ); + Assert.ok( + IPPExceptionsManager.inclusions.has(validSite), + `inclusions set should include ${validSite}` + ); + + let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF); + + Assert.ok(newStringPref.includes(site1), `String pref includes ${site1}`); + Assert.ok( + newStringPref.includes(validSite), + `String pref includes ${validSite}` + ); + + Services.prefs.clearUserPref(MODE_PREF); + Services.prefs.clearUserPref(INCLUSIONS_PREF); + IPPExceptionsManager.uninit(); +}); + +/** + * Tests that we can remove domains from the inclusions set. + */ +add_task(async function test_IPPExceptionsManager_remove_inclusions() { + const site1 = "https://www.example.com"; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + Services.prefs.setStringPref(INCLUSIONS_PREF, site1); + + IPPExceptionsManager.init(); + + Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); + + const invalidSite = "urlDoesntExist"; + + IPPExceptionsManager.removeException(site1); + IPPExceptionsManager.removeException(invalidSite); + + Assert.ok( + !IPPExceptionsManager.inclusions.size, + "inclusions set should be empty" + ); + + let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF); + + Assert.ok(!newStringPref, "String pref should be empty"); + + Services.prefs.clearUserPref(MODE_PREF); + Services.prefs.clearUserPref(INCLUSIONS_PREF); + IPPExceptionsManager.uninit(); +}); diff --git a/browser/components/ipprotection/tests/xpcshell/xpcshell.toml b/browser/components/ipprotection/tests/xpcshell/xpcshell.toml @@ -11,6 +11,8 @@ prefs = [ ["test_IPPExceptionsManager_exclusions.js"] +["test_IPPExceptionsManager_inclusions.js"] + ["test_IPPStartupCache.js"] ["test_IPProtection.js"]