tor-browser

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

commit c7dd1efaff8180014af79b6db9aaebc30ab2a7ec
parent e4049a13c32bfb3f3b22368969721ac216bc6f0b
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date:   Mon, 10 Nov 2025 14:06:22 +0000

Bug 1998795 - IPProtection: implement the ACTIVATING state - part 4 - Activating state, r=ip-protection-reviewers,sstreich

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

Diffstat:
Mbrowser/components/ipprotection/IPPProxyManager.sys.mjs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mbrowser/components/ipprotection/tests/browser/browser_IPPProxyManager.js | 2+-
Mbrowser/components/ipprotection/tests/browser/browser_IPProtectionService.js | 2+-
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js | 2+-
Mbrowser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
5 files changed, 154 insertions(+), 36 deletions(-)

diff --git a/browser/components/ipprotection/IPPProxyManager.sys.mjs b/browser/components/ipprotection/IPPProxyManager.sys.mjs @@ -50,6 +50,7 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { export const IPPProxyStates = Object.freeze({ NOT_READY: "not-ready", READY: "ready", + ACTIVATING: "activating", ACTIVE: "active", ERROR: "error", }); @@ -60,6 +61,8 @@ export const IPPProxyStates = Object.freeze({ class IPPProxyManagerSingleton extends EventTarget { #state = IPPProxyStates.NOT_READY; + #activatingPromise = null; + #pass = null; /**@type {import("./IPPChannelFilter.sys.mjs").IPPChannelFilter | null} */ #connection = null; @@ -96,7 +99,10 @@ class IPPProxyManagerSingleton extends EventTarget { this.errors = []; - if (this.#state === IPPProxyStates.ACTIVE) { + if ( + this.#state === IPPProxyStates.ACTIVE || + this.#state === IPPProxyStates.ACTIVATING + ) { this.stop(false); } @@ -173,34 +179,50 @@ class IPPProxyManagerSingleton extends EventTarget { throw new Error("This method should not be called when not ready"); } - let started = false; - try { - started = await this.#startInternal(); - } catch (error) { - this.#setErrorState(ERRORS.GENERIC, error); - return; - } + if (this.#state === IPPProxyStates.ACTIVATING) { + if (!this.#activatingPromise) { + throw new Error("Activating without a promise?!?"); + } - if (this.#state === IPPProxyStates.ERROR) { - return; + return this.#activatingPromise; } - // Proxy failed to start but no error was given. - if (!started) { - this.#setState(IPPProxyStates.READY); - return; - } - - this.#setState(IPPProxyStates.ACTIVE); - - Glean.ipprotection.toggled.record({ - userAction, - enabled: true, - }); - - if (userAction) { - this.#reloadCurrentTab(); - } + const activating = async () => { + let started = false; + try { + started = await this.#startInternal(); + } catch (error) { + this.#setErrorState(ERRORS.GENERIC, error); + return; + } + + if (this.#state === IPPProxyStates.ERROR) { + return; + } + + // Proxy failed to start but no error was given. + if (!started) { + this.#setState(IPPProxyStates.READY); + return; + } + + this.#setState(IPPProxyStates.ACTIVE); + + Glean.ipprotection.toggled.record({ + userAction, + enabled: true, + }); + + if (userAction) { + this.#reloadCurrentTab(); + } + }; + + this.#setState(IPPProxyStates.ACTIVATING); + this.#activatingPromise = activating().finally( + () => (this.#activatingPromise = null) + ); + return this.#activatingPromise; } async #startInternal() { @@ -271,6 +293,15 @@ class IPPProxyManagerSingleton extends EventTarget { * True if started by user action, false if system action */ async stop(userAction = true) { + if (this.#state === IPPProxyStates.ACTIVATING) { + if (!this.#activatingPromise) { + throw new Error("Activating without a promise?!?"); + } + + await this.#activatingPromise.then(() => this.stop(userAction)); + return; + } + if (this.#state !== IPPProxyStates.ACTIVE) { return; } @@ -311,7 +342,10 @@ class IPPProxyManagerSingleton extends EventTarget { */ async reset() { this.#pass = null; - if (this.#state === IPPProxyStates.ACTIVE) { + if ( + this.#state === IPPProxyStates.ACTIVE || + this.#state === IPPProxyStates.ACTIVATING + ) { await this.stop(); } } diff --git a/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js b/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js @@ -97,7 +97,7 @@ add_task(async function test_IPPProxyManager_handleProxyErrorEvent() { // Test inactive connection const isolationKeyBeforeStop = IPPProxyManager.isolationKey; - IPPProxyManager.stop(); + await IPPProxyManager.stop(); const inactiveErrorEvent = new CustomEvent("proxy-http-error", { detail: { diff --git a/browser/components/ipprotection/tests/browser/browser_IPProtectionService.js b/browser/components/ipprotection/tests/browser/browser_IPProtectionService.js @@ -346,7 +346,7 @@ add_task(async function test_IPProtectionService_retry_errors() { Assert.equal(IPPProxyManager.state, IPPProxyStates.ACTIVE, "Proxy is active"); - IPPProxyManager.stop(); + await IPPProxyManager.stop(); await closePanel(); await cleanupAlpha(); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js @@ -276,7 +276,7 @@ add_task(async function test_IPProtectionPanel_started_stopped() { () => IPPProxyManager.state !== IPPProxyStates.ACTIVE ); - IPPProxyManager.stop(); + await IPPProxyManager.stop(); await stoppedEventPromise; diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js b/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js @@ -40,6 +40,12 @@ add_task(async function test_IPPProxyManager_start() { IPPProxyManager.start(); + Assert.equal( + IPPProxyManager.state, + IPPProxyStates.ACTIVATING, + "Proxy activation" + ); + await startedEventPromise; Assert.equal( @@ -81,14 +87,14 @@ add_task(async function test_IPPProxyManager_stop() { let stoppedEventPromise = waitForEvent( IPPProxyManager, "IPPProxyManager:StateChanged", - () => IPPProxyManager.state !== IPPProxyStates.ACTIVE + () => IPPProxyManager.state === IPPProxyStates.READY ); - IPPProxyManager.stop(); + await IPPProxyManager.stop(); await stoppedEventPromise; - Assert.notEqual( + Assert.equal( IPPProxyManager.state, - IPPProxyStates.ACTIVE, + IPPProxyStates.READY, "IP Protection service should not be active after stopping" ); Assert.ok( @@ -135,7 +141,7 @@ add_task(async function test_IPPProxyManager_start_stop_reset() { "Should have a valid proxy pass after starting" ); - IPPProxyManager.stop(); + await IPPProxyManager.stop(); Assert.ok(!IPPProxyManager.active, "Should not be active after starting"); @@ -270,7 +276,15 @@ add_task(async function test_IPPProxytates_active() { "IP Protection service should be ready" ); - await IPPProxyManager.start(false); + const startPromise = IPPProxyManager.start(false); + + Assert.equal( + IPPProxyManager.state, + IPPProxyStates.ACTIVATING, + "Proxy activation" + ); + + await startPromise; Assert.equal( IPProtectionService.state, @@ -295,3 +309,73 @@ add_task(async function test_IPPProxytates_active() { IPProtectionService.uninit(); sandbox.restore(); }); + +/** + * Tests the quick start/stop calls. + */ +add_task(async function test_IPPProxytates_start_stop() { + let sandbox = sinon.createSandbox(); + sandbox.stub(IPPSignInWatcher, "isSignedIn").get(() => true); + sandbox + .stub(IPProtectionService.guardian, "isLinkedToGuardian") + .resolves(true); + sandbox.stub(IPProtectionService.guardian, "fetchUserInfo").resolves({ + status: 200, + error: undefined, + entitlement: { uid: 42 }, + }); + sandbox.stub(IPProtectionService.guardian, "fetchProxyPass").resolves({ + status: 200, + error: undefined, + pass: { + isValid: () => options.validProxyPass, + asBearerToken: () => "Bearer helloworld", + }, + }); + + const waitForReady = waitForEvent( + IPProtectionService, + "IPProtectionService:StateChanged", + () => IPProtectionService.state === IPProtectionStates.READY + ); + + IPProtectionService.init(); + + await waitForReady; + + Assert.equal( + IPProtectionService.state, + IPProtectionStates.READY, + "IP Protection service should be ready" + ); + + IPPProxyManager.start(false); + IPPProxyManager.start(false); + IPPProxyManager.start(false); + + IPPProxyManager.stop(false); + IPPProxyManager.stop(false); + IPPProxyManager.stop(false); + IPPProxyManager.stop(false); + + Assert.equal( + IPPProxyManager.state, + IPPProxyStates.ACTIVATING, + "Proxy activation" + ); + + await waitForEvent( + IPPProxyManager, + "IPPProxyManager:StateChanged", + () => IPPProxyManager.state === IPPProxyStates.ACTIVE + ); + + await waitForEvent( + IPPProxyManager, + "IPPProxyManager:StateChanged", + () => IPPProxyManager.state === IPPProxyStates.READY + ); + + IPProtectionService.uninit(); + sandbox.restore(); +});