tor-browser

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

commit 915e56bb1277a90f46ef72a1b68cf725c1ce3753
parent 3877900f35e2d0976a296aa4bb83588a21990fa4
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date:   Mon, 13 Oct 2025 16:45:57 +0000

Bug 1993175 - Introduce a startup-cache for IP Protection service, r=ip-protection-reviewers,fchasen

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

Diffstat:
Mbrowser/components/ipprotection/IPPSignInWatcher.sys.mjs | 21++++++---------------
Abrowser/components/ipprotection/IPPStartupCache.sys.mjs | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/IPProtectionHelpers.sys.mjs | 14++++++++++++--
Mbrowser/components/ipprotection/IPProtectionService.sys.mjs | 25++++++++++++++++++++++++-
Mbrowser/components/ipprotection/moz.build | 1+
Abrowser/components/ipprotection/tests/xpcshell/test_IPPStartupCache.js | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/tests/xpcshell/xpcshell.toml | 3+++
7 files changed, 377 insertions(+), 18 deletions(-)

diff --git a/browser/components/ipprotection/IPPSignInWatcher.sys.mjs b/browser/components/ipprotection/IPPSignInWatcher.sys.mjs @@ -10,12 +10,6 @@ ChromeUtils.defineESModuleGetters(lazy, { UIState: "resource://services-sync/UIState.sys.mjs", }); -ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { - return ChromeUtils.importESModule( - "resource://gre/modules/FxAccounts.sys.mjs" - ).getFxAccountsSingleton(); -}); - /** * This class monitors the Sign-In state and triggers the update of the service * if needed. @@ -31,10 +25,14 @@ class IPPSignInWatcherSingleton { this.#signedIn = signedIn; } + init() { + this.#signedIn = Services.prefs.prefHasUserValue("services.sync.username"); + } + /** - * Adds an observer for the FxA sign-in state. + * Adds an observer for the FxA sign-in state, only when the browser is fully started. */ - async init() { + async initOnStartupCompleted() { this.fxaObserver = { QueryInterface: ChromeUtils.generateQI([ Ci.nsIObserver, @@ -52,13 +50,6 @@ class IPPSignInWatcherSingleton { }; Services.obs.addObserver(this.fxaObserver, lazy.UIState.ON_UPDATE); - - // Let's check the sign-in state only if we have seen the user signed in - // once at least. - if (Services.prefs.prefHasUserValue("services.sync.username")) { - const userData = await lazy.fxAccounts.getSignedInUser(); - this.#signedIn = userData?.verified || false; - } } /** diff --git a/browser/components/ipprotection/IPPStartupCache.sys.mjs b/browser/components/ipprotection/IPPStartupCache.sys.mjs @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + IPProtectionService: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", + IPProtectionStates: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", +}); + +const STATE_CACHE_PREF = "browser.ipProtection.stateCache"; +const ENTITLEMENT_CACHE_PREF = "browser.ipProtection.entitlementCache"; + +/** + * This class implements a cache for the IPP state machine. The cache is used + * until we receive the `sessionstore-windows-restored` event + */ +class IPPStartupCacheSingleton { + #stateFromCache = null; + #startupCompleted = false; + + constructor() { + // For XPCShell tests, the cache must be disabled. + if ( + Services.prefs.getBoolPref("browser.ipProtection.cacheDisabled", false) + ) { + this.#startupCompleted = true; + return; + } + + this.handleEvent = this.#handleEvent.bind(this); + + const stateFromCache = Services.prefs.getCharPref( + STATE_CACHE_PREF, + "unset" + ); + if (stateFromCache !== "unset") { + this.#stateFromCache = stateFromCache; + } + + Services.obs.addObserver(this, "sessionstore-windows-restored"); + } + + init() { + lazy.IPProtectionService.addEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + + // The state cannot be "ACTIVE" from cache. In case we need to activate the + // proxy at startup time, something else will take care of it. + if (this.#stateFromCache === lazy.IPProtectionStates.ACTIVE) { + this.#stateFromCache = lazy.IPProtectionStates.READY; + } + } + + async initOnStartupCompleted() {} + + uninit() { + lazy.IPProtectionService.removeEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + get isStartupCompleted() { + return this.#startupCompleted; + } + + get state() { + if (this.#startupCompleted) { + throw new Error("IPPStartupCache should not be used after the startup"); + } + + if (Object.values(lazy.IPProtectionStates).includes(this.#stateFromCache)) { + return this.#stateFromCache; + } + + // This should not happen. + return lazy.IPProtectionStates.UNINITIALIZED; + } + + async observe(_subject, topic, _) { + if (topic !== "sessionstore-windows-restored") { + return; + } + + // The browser is ready! Let's invalidate the cache and let's recompute the + // state. + + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + this.#startupCompleted = true; + this.#stateFromCache = null; + + await lazy.IPProtectionService.initOnStartupCompleted(); + lazy.IPProtectionService.updateState(); + } + + storeEntitlement(entitlement) { + Services.prefs.setCharPref( + ENTITLEMENT_CACHE_PREF, + JSON.stringify(entitlement) + ); + } + + get entitlement() { + try { + const entitlement = Services.prefs.getCharPref( + ENTITLEMENT_CACHE_PREF, + "" + ); + return JSON.parse(entitlement); + } catch (e) { + return null; + } + } + + #handleEvent(_event) { + const state = lazy.IPProtectionService.state; + if (this.#startupCompleted) { + Services.prefs.setCharPref(STATE_CACHE_PREF, state); + } else { + this.#stateFromCache = state; + } + } +} + +const IPPStartupCache = new IPPStartupCacheSingleton(); + +export { IPPStartupCache, IPPStartupCacheSingleton }; diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs @@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, { }); import { IPPSignInWatcher } from "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs"; +import { IPPStartupCache } from "resource:///modules/ipprotection/IPPStartupCache.sys.mjs"; const VPN_ADDON_ID = "vpn@mozilla.com"; @@ -38,6 +39,8 @@ class UIHelper { ); } + initOnStartupCompleted() {} + uninit() { lazy.IPProtectionService.removeEventListener( "IPProtectionService:StateChanged", @@ -76,6 +79,8 @@ class AccountResetHelper { ); } + initOnStartupCompleted() {} + uninit() { lazy.IPProtectionService.removeEventListener( "IPProtectionService:StateChanged", @@ -101,10 +106,12 @@ class AccountResetHelper { * This class removes the UI widget if the VPN add-on is installed. */ class VPNAddonHelper { + init() {} + /** * Adds an observer to monitor the VPN add-on installation */ - init() { + initOnStartupCompleted() { this.addonVPNListener = { onInstallEnded(_install, addon) { if (addon.id === VPN_ADDON_ID && lazy.IPProtectionService.hasUpgraded) { @@ -133,7 +140,9 @@ class VPNAddonHelper { * This class monitors the eligibility flag from Nimbus */ class EligibilityHelper { - init() { + init() {} + + initOnStartupCompleted() { lazy.NimbusFeatures.ipProtection.onUpdate( lazy.IPProtectionService.updateState ); @@ -147,6 +156,7 @@ class EligibilityHelper { } const IPPHelpers = [ + IPPStartupCache, new AccountResetHelper(), new EligibilityHelper(), new VPNAddonHelper(), diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs @@ -11,6 +11,7 @@ ChromeUtils.defineESModuleGetters(lazy, { IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs", IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs", IPPSignInWatcher: "resource:///modules/ipprotection/IPPSignInWatcher.sys.mjs", + IPPStartupCache: "resource:///modules/ipprotection/IPPStartupCache.sys.mjs", SpecialMessageActions: "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", @@ -140,11 +141,17 @@ class IPProtectionServiceSingleton extends EventTarget { if (this.#state !== IPProtectionStates.UNINITIALIZED) { return; } + this.proxyManager = new lazy.IPPProxyManager(this.guardian); + this.#entitlement = lazy.IPPStartupCache.entitlement; - await Promise.allSettled(this.#helpers.map(helper => helper.init())); + this.#helpers.forEach(helper => helper.init()); await this.#updateState(); + + if (lazy.IPPStartupCache.isStartupCompleted) { + this.initOnStartupCompleted(); + } } /** @@ -169,6 +176,12 @@ class IPProtectionServiceSingleton extends EventTarget { this.#setState(IPProtectionStates.UNINITIALIZED); } + async initOnStartupCompleted() { + await Promise.allSettled( + this.#helpers.map(helper => helper.initOnStartupCompleted()) + ); + } + /** * Start the proxy if the user is eligible. * @@ -267,6 +280,8 @@ class IPProtectionServiceSingleton extends EventTarget { */ resetAccount() { this.#entitlement = null; + lazy.IPPStartupCache.storeEntitlement(null); + if (this.proxyManager?.active) { this.stop(false); } @@ -317,6 +332,8 @@ class IPProtectionServiceSingleton extends EventTarget { async refetchEntitlement() { let prevState = this.#state; this.#entitlement = null; + lazy.IPPStartupCache.storeEntitlement(null); + await this.#updateState(); // hasUpgraded might not change the state. if (prevState === this.#state) { @@ -383,6 +400,7 @@ class IPProtectionServiceSingleton extends EventTarget { // Entitlement is set until the user changes or it is cleared to check subscription status. this.#entitlement = entitlement; + lazy.IPPStartupCache.storeEntitlement(entitlement); return entitlement; } @@ -419,6 +437,11 @@ class IPProtectionServiceSingleton extends EventTarget { return IPProtectionStates.UNINITIALIZED; } + // Maybe we have to use the cached state, because we are not initialized yet. + if (!lazy.IPPStartupCache.isStartupCompleted) { + return lazy.IPPStartupCache.state; + } + // For non authenticated users, we can check if they are eligible (the UI // is shown and they have to login) or we don't know yet their current // enroll state (no UI is shown). diff --git a/browser/components/ipprotection/moz.build b/browser/components/ipprotection/moz.build @@ -22,6 +22,7 @@ EXTRA_JS_MODULES.ipprotection += [ "IPProtectionService.sys.mjs", "IPProtectionUsage.sys.mjs", "IPPSignInWatcher.sys.mjs", + "IPPStartupCache.sys.mjs", ] BROWSER_CHROME_MANIFESTS += [ diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPStartupCache.js b/browser/components/ipprotection/tests/xpcshell/test_IPPStartupCache.js @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { IPPStartupCacheSingleton } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPPStartupCache.sys.mjs" +); + +const { IPProtectionStates } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPProtectionService.sys.mjs" +); + +/** + * Test the disabled cache + */ +add_task(async function test_IPPStartupCache_disabled() { + // By default the cache is not active. + Services.prefs.setBoolPref("browser.ipProtection.cacheDisabled", true); + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + cache.isStartupCompleted, + "In XPCShell mode the cache is not active" + ); +}); + +/** + * Test the enabled cache + */ +add_task(async function test_IPPStartupCache_enabled() { + // By default the cache is not active. + Services.prefs.setBoolPref("browser.ipProtection.cacheDisabled", false); + + // Default state is UNINITIALIZED + { + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal( + cache.state, + IPProtectionStates.UNINITIALIZED, + "The state is unitialized" + ); + } + + // Fetch the cached state + { + Services.prefs.setCharPref( + "browser.ipProtection.stateCache", + IPProtectionStates.READY + ); + + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal(cache.state, IPProtectionStates.READY, "The state is READY"); + } + + // Invalid cache means UNINITIALIZED + { + Services.prefs.setCharPref( + "browser.ipProtection.stateCache", + "Hello World!" + ); + + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal( + cache.state, + IPProtectionStates.UNINITIALIZED, + "The state is unitialized" + ); + } + + // ACTIVE to READY + { + Services.prefs.setCharPref( + "browser.ipProtection.stateCache", + IPProtectionStates.ACTIVE + ); + + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal(cache.state, IPProtectionStates.READY, "The state is READY"); + } +}); + +/** + * Cache the entitlement + */ +add_task(async function test_IPPStartupCache_enabled() { + // By default the cache is not active. + Services.prefs.setBoolPref("browser.ipProtection.cacheDisabled", false); + + // Default entitlement is null + { + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal(cache.entitlement, null, "Null entitlement"); + } + + // A JSON object for entitlement + { + Services.prefs.setCharPref( + "browser.ipProtection.entitlementCache", + '{"a": 42}' + ); + + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.deepEqual( + cache.entitlement, + { a: 42 }, + "A valid entitlement object" + ); + } + + // Invalid JSON + { + Services.prefs.setCharPref( + "browser.ipProtection.entitlementCache", + '{"a": 42}}}}' + ); + + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal(cache.entitlement, null, "Null entitlement"); + } + + // Setter + { + const cache = new IPPStartupCacheSingleton(); + cache.init(); + + Assert.ok( + !cache.isStartupCompleted, + "In XPCShell mode the cache is active" + ); + Assert.equal(cache.entitlement, null, "Null entitlement"); + + cache.storeEntitlement(42); + Assert.equal( + Services.prefs.getCharPref("browser.ipProtection.entitlementCache", ""), + "42", + "The cache is correctly stored (number)" + ); + + cache.storeEntitlement(null); + Assert.equal( + Services.prefs.getCharPref("browser.ipProtection.entitlementCache", ""), + "null", + "The cache is correctly stored (null)" + ); + + cache.storeEntitlement({ a: 42 }); + Assert.equal( + Services.prefs.getCharPref("browser.ipProtection.entitlementCache", ""), + '{"a":42}', + "The cache is correctly stored (obj)" + ); + } +}); diff --git a/browser/components/ipprotection/tests/xpcshell/xpcshell.toml b/browser/components/ipprotection/tests/xpcshell/xpcshell.toml @@ -4,12 +4,15 @@ head = "head.js" firefox-appdir = "browser" prefs = [ "browser.ipProtection.enabled=true", + "browser.ipProtection.cacheDisabled=true", ] ["test_GuardianClient.js"] ["test_IPPExceptionsManager_exclusions.js"] +["test_IPPStartupCache.js"] + ["test_IPProtection.js"] ["test_IPProtectionPanel.js"]