tor-browser

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

commit 7f3b1bfeb924d3db10d047be394882a8e2eee110
parent 936fdf8ebfa062e69864b34fee89521500eaa031
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date:   Wed,  1 Oct 2025 14:10:14 +0000

Bug 1991875 - Create helper classes to cleanup the IPProtectionService, r=ip-protection-reviewers,rking

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

Diffstat:
Mbrowser/components/ipprotection/IPPProxyManager.sys.mjs | 17+++++++++++++----
Abrowser/components/ipprotection/IPProtectionHelpers.sys.mjs | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/IPProtectionService.sys.mjs | 135++++++++++++-------------------------------------------------------------------
Mbrowser/components/ipprotection/moz.build | 1+
4 files changed, 224 insertions(+), 119 deletions(-)

diff --git a/browser/components/ipprotection/IPPProxyManager.sys.mjs b/browser/components/ipprotection/IPPProxyManager.sys.mjs @@ -38,6 +38,11 @@ class IPPProxyManager { #networkErrorObserver = null; // If this is set, we're awaiting a proxy pass rotation #rotateProxyPassPromise = null; + #activatedAt = false; + + get activatedAt() { + return this.#activatedAt; + } get guardian() { if (!this.#guardian) { @@ -119,22 +124,26 @@ class IPPProxyManager { lazy.logConsole.info("Started"); + if (this.active) { + this.#activatedAt = ChromeUtils.now(); + } + return this.active; } /** - * Stops the proxy connection and observers. + * Stops the proxy connection and observers. Returns the duration of the connection. * - * @returns {Promise<boolean>} + * @returns {int} */ - async stop() { + stop() { this.#connection?.stop(); this.networkErrorObserver.stop(); this.#connection = null; lazy.logConsole.info("Stopped"); - return true; + return ChromeUtils.now() - this.#activatedAt; } /** diff --git a/browser/components/ipprotection/IPProtectionHelpers.sys.mjs b/browser/components/ipprotection/IPProtectionHelpers.sys.mjs @@ -0,0 +1,190 @@ +/* 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, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + CustomizableUI: + "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", + IPProtection: "resource:///modules/ipprotection/IPProtection.sys.mjs", + IPProtectionWidget: "resource:///modules/ipprotection/IPProtection.sys.mjs", + IPProtectionService: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", + IPProtectionStates: + "resource:///modules/ipprotection/IPProtectionService.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + UIState: "resource://services-sync/UIState.sys.mjs", +}); + +const VPN_ADDON_ID = "vpn@mozilla.com"; + +/** + * This simple class controls the UI activation/deactivation. + */ +class UIHelper { + constructor() { + this.handleEvent = this.#handleEvent.bind(this); + } + + init() { + lazy.IPProtectionService.addEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + uninit() { + lazy.IPProtectionService.removeEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + lazy.IPProtection.uninit(); + } + + #handleEvent(_event) { + const state = lazy.IPProtectionService.state; + + if ( + !lazy.IPProtection.isInitialized && + state !== lazy.IPProtectionStates.UNINITIALIZED && + state !== lazy.IPProtectionStates.UNAVAILABLE + ) { + lazy.IPProtection.init(); + } + } +} + +/** + * This simple class resets the account data when needed + */ +class AccountResetHelper { + constructor() { + this.handleEvent = this.#handleEvent.bind(this); + } + + init() { + lazy.IPProtectionService.addEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + uninit() { + lazy.IPProtectionService.removeEventListener( + "IPProtectionService:StateChanged", + this.handleEvent + ); + } + + #handleEvent(_event) { + // Reset stored account information and stop the proxy, + // if the account is no longer available. + if ( + (lazy.IPProtectionService.hasEntitlement && + lazy.IPProtectionService.state === + lazy.IPProtectionStates.UNAVAILABLE) || + lazy.IPProtectionService.state === lazy.IPProtectionStates.UNAUTHENTICATED + ) { + lazy.IPProtectionService.resetAccount(); + } + } +} + +/** + * This class removes the UI widget if the VPN add-on is installed. + */ +class VPNAddonHelper { + /** + * Adds an observer to monitor the VPN add-on installation + */ + init() { + this.addonVPNListener = { + onInstallEnded(_install, addon) { + if (addon.id === VPN_ADDON_ID && lazy.IPProtectionService.hasUpgraded) { + // Place the widget in the customization palette. + lazy.CustomizableUI.removeWidgetFromArea( + lazy.IPProtectionWidget.WIDGET_ID + ); + } + }, + }; + + lazy.AddonManager.addInstallListener(this.addonVPNListener); + } + + /** + * Removes the VPN add-on installation observer + */ + uninit() { + if (this.addonVPNListener) { + lazy.AddonManager.removeInstallListener(this.addonVPNListener); + } + } +} + +/** + * This class monitors the Sign-In state and triggers the update of the service + * if needed. + */ +class SignInStateHelper { + /** + * Adds an observer for the FxA sign-in state. + */ + init() { + this.fxaObserver = { + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]), + + observe() { + let { status } = lazy.UIState.get(); + let signedIn = status == lazy.UIState.STATUS_SIGNED_IN; + if (signedIn !== lazy.IPProtectionService.signedIn) { + lazy.IPProtectionService.updateState(); + } + }, + }; + + Services.obs.addObserver(this.fxaObserver, lazy.UIState.ON_UPDATE); + } + + /** + * Removes the FxA sign-in state observer + */ + uninit() { + if (this.fxaObserver) { + Services.obs.removeObserver(this.fxaObserver, lazy.UIState.ON_UPDATE); + this.fxaObserver = null; + } + } +} + +/** + * This class monitors the eligibility flag from Nimbus + */ +class EligibilityHelper { + init() { + lazy.NimbusFeatures.ipProtection.onUpdate( + lazy.IPProtectionService.updateState + ); + } + + uninit() { + lazy.NimbusFeatures.ipProtection.offUpdate( + lazy.IPProtectionService.updateState + ); + } +} + +const IPPHelpers = [ + new AccountResetHelper(), + new EligibilityHelper(), + new SignInStateHelper(), + new VPNAddonHelper(), + new UIHelper(), +]; + +export { IPPHelpers }; diff --git a/browser/components/ipprotection/IPProtectionService.sys.mjs b/browser/components/ipprotection/IPProtectionService.sys.mjs @@ -7,16 +7,13 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - AddonManager: "resource://gre/modules/AddonManager.sys.mjs", GuardianClient: "resource:///modules/ipprotection/GuardianClient.sys.mjs", + IPPHelpers: "resource:///modules/ipprotection/IPProtectionHelpers.sys.mjs", IPPProxyManager: "resource:///modules/ipprotection/IPPProxyManager.sys.mjs", UIState: "resource://services-sync/UIState.sys.mjs", SpecialMessageActions: "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", - IPProtection: "resource:///modules/ipprotection/IPProtection.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", - CustomizableUI: - "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", }); import { @@ -26,7 +23,6 @@ import { const ENABLED_PREF = "browser.ipProtection.enabled"; const LOG_PREF = "browser.ipProtection.log"; -const VPN_ADDON_ID = "vpn@mozilla.com"; const MAX_ERROR_HISTORY = 50; ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { @@ -73,9 +69,6 @@ export const IPProtectionStates = Object.freeze({ * know the current state. */ class IPProtectionServiceSingleton extends EventTarget { - static WIDGET_ID = "ipprotection-button"; - static PANEL_ID = "PanelUI-ipprotection"; - #state = IPProtectionStates.UNINITIALIZED; // Prevents multiple `#updateState()` executions at once. @@ -89,7 +82,8 @@ class IPProtectionServiceSingleton extends EventTarget { proxyManager = null; #entitlement = null; - #activatedAt = false; + + #helpers = null; /** * Returns the state of the service. See the description of the state @@ -111,12 +105,21 @@ class IPProtectionServiceSingleton extends EventTarget { } /** + * Checks if the service has an entitlement object + * + * @returns {boolean} + */ + get hasEntitlement() { + return !!this.#entitlement; + } + + /** * Checks if the proxy is active and was activated. * * @returns {Date} */ get activatedAt() { - return this.proxyManager?.active && this.#activatedAt; + return this.proxyManager?.active && this.proxyManager?.activatedAt; } constructor() { @@ -127,6 +130,8 @@ class IPProtectionServiceSingleton extends EventTarget { this.updateState = this.#updateState.bind(this); this.setState = this.#setState.bind(this); this.setErrorState = this.#setErrorState.bind(this); + + this.#helpers = lazy.IPPHelpers; } /** @@ -138,38 +143,31 @@ class IPProtectionServiceSingleton extends EventTarget { } this.proxyManager = new lazy.IPPProxyManager(this.guardian); - this.#addSignInStateObserver(); - this.addVPNAddonObserver(); - this.#addEligibilityListeners(); + this.#helpers.forEach(helper => helper.init()); await this.#updateState(); } /** - * Removes the IPProtectionService and IPProtection widget. + * Removes the UI widget. */ uninit() { if (this.#state === IPProtectionStates.UNINITIALIZED) { return; } - lazy.IPProtection.uninit(); - - this.#removeSignInStateObserver(); - this.removeVPNAddonObserver(); - if (this.#state === IPProtectionStates.ACTIVE) { this.stop(false); } this.proxyManager?.destroy(); - this.#removeEligibilityListeners(); - this.#entitlement = null; this.errors = []; this.enrolling = null; this.signedIn = null; + this.#helpers.forEach(helper => helper.uninit()); + this.#setState(IPProtectionStates.UNINITIALIZED); } @@ -206,8 +204,6 @@ class IPProtectionServiceSingleton extends EventTarget { return; } - this.#activatedAt = ChromeUtils.now(); - this.#setState(IPProtectionStates.ACTIVE); Glean.ipprotection.toggled.record({ @@ -231,9 +227,7 @@ class IPProtectionServiceSingleton extends EventTarget { return; } - let deactivatedAt = ChromeUtils.now(); - let sessionLength = deactivatedAt - this.#activatedAt; - this.#activatedAt = null; + const sessionLength = this.proxyManager.stop(); Glean.ipprotection.toggled.record({ userAction, @@ -241,7 +235,6 @@ class IPProtectionServiceSingleton extends EventTarget { enabled: false, }); - await this.proxyManager.stop(); this.#setState(IPProtectionStates.READY); this.dispatchEvent( @@ -336,14 +329,6 @@ class IPProtectionServiceSingleton extends EventTarget { return isEligible; } - #addEligibilityListeners() { - lazy.NimbusFeatures.ipProtection.onUpdate(this.updateState); - } - - #removeEligibilityListeners() { - lazy.NimbusFeatures.ipProtection.offUpdate(this.updateState); - } - /** * Clear the current entitlement and requests a state update to dispatch * the current hasUpgraded status. @@ -361,68 +346,6 @@ class IPProtectionServiceSingleton extends EventTarget { } /** - * Adds an observer for the FxA sign-in state. - */ - #addSignInStateObserver() { - let manager = this; - this.fxaObserver = { - QueryInterface: ChromeUtils.generateQI([ - Ci.nsIObserver, - Ci.nsISupportsWeakReference, - ]), - - observe() { - let { status } = lazy.UIState.get(); - let signedIn = status == lazy.UIState.STATUS_SIGNED_IN; - if (signedIn !== manager.signedIn) { - manager.updateState(); - } - }, - }; - - Services.obs.addObserver(this.fxaObserver, lazy.UIState.ON_UPDATE); - } - - /** - * Removes the FxA sign-in state observer - */ - #removeSignInStateObserver() { - if (this.fxaObserver) { - Services.obs.removeObserver(this.fxaObserver, lazy.UIState.ON_UPDATE); - this.fxaObserver = null; - } - } - - /** - * Adds an observer to monitor the VPN add-on installation - */ - addVPNAddonObserver() { - let service = this; - this.addonVPNListener = { - onInstallEnded(_install, addon) { - if (addon.id === VPN_ADDON_ID && service.hasUpgraded) { - // Place the widget in the customization palette. - lazy.CustomizableUI.removeWidgetFromArea( - IPProtectionServiceSingleton.WIDGET_ID - ); - lazy.logConsole.info("VPN Extension: Installed"); - } - }, - }; - - lazy.AddonManager.addInstallListener(this.addonVPNListener); - } - - /** - * Removes the VPN add-on installation observer - */ - removeVPNAddonObserver() { - if (this.addonVPNListener) { - lazy.AddonManager.removeInstallListener(this.addonVPNListener); - } - } - - /** * Enrolls a users FxA account to use the proxy and updates the state. * * @returns {Promise<void>} @@ -584,24 +507,6 @@ class IPProtectionServiceSingleton extends EventTarget { * @param {IPProtectionStates} prevState */ #stateChanged(state, prevState) { - // Reset stored account information and stop the proxy, - // if the account is no longer available. - if ( - (this.#entitlement && state === IPProtectionStates.UNAVAILABLE) || - state === IPProtectionStates.UNAUTHENTICATED - ) { - this.resetAccount(); - } - - // Add the IPProtection widget if needed. - if ( - !lazy.IPProtection.isInitialized && - state !== IPProtectionStates.UNINITIALIZED && - state !== IPProtectionStates.UNAVAILABLE - ) { - lazy.IPProtection.init(); - } - this.dispatchEvent( new CustomEvent("IPProtectionService:StateChanged", { bubbles: true, diff --git a/browser/components/ipprotection/moz.build b/browser/components/ipprotection/moz.build @@ -15,6 +15,7 @@ EXTRA_JS_MODULES.ipprotection += [ "IPPNetworkErrorObserver.sys.mjs", "IPPProxyManager.sys.mjs", "IPProtection.sys.mjs", + "IPProtectionHelpers.sys.mjs", "IPProtectionPanel.sys.mjs", "IPProtectionServerlist.sys.mjs", "IPProtectionService.sys.mjs",