IPProtectionService.sys.mjs (7135B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 GuardianClient: 11 "moz-src:///browser/components/ipprotection/GuardianClient.sys.mjs", 12 IPPEnrollAndEntitleManager: 13 "moz-src:///browser/components/ipprotection/IPPEnrollAndEntitleManager.sys.mjs", 14 IPPHelpers: 15 "moz-src:///browser/components/ipprotection/IPProtectionHelpers.sys.mjs", 16 IPPNimbusHelper: 17 "moz-src:///browser/components/ipprotection/IPPNimbusHelper.sys.mjs", 18 IPPOptOutHelper: 19 "moz-src:///browser/components/ipprotection/IPPOptOutHelper.sys.mjs", 20 IPPSignInWatcher: 21 "moz-src:///browser/components/ipprotection/IPPSignInWatcher.sys.mjs", 22 IPPStartupCache: 23 "moz-src:///browser/components/ipprotection/IPPStartupCache.sys.mjs", 24 IPPVPNAddonHelper: 25 "moz-src:///browser/components/ipprotection/IPPVPNAddonHelper.sys.mjs", 26 SpecialMessageActions: 27 "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", 28 }); 29 30 import { SIGNIN_DATA } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; 31 32 const ENABLED_PREF = "browser.ipProtection.enabled"; 33 34 /** 35 * @typedef {object} IPProtectionStates 36 * List of the possible states of the IPProtectionService. 37 * @property {string} UNINITIALIZED 38 * The service has not been initialized yet. 39 * @property {string} UNAVAILABLE 40 * The user is not eligible (via nimbus) or still not signed in. No UI is available. 41 * @property {string} UNAUTHENTICATED 42 * The user is signed out but eligible (via nimbus). The panel should show the login view. 43 * @property {string} OPTED_OUT 44 * The user has opted out from using VPN. The toolbar icon and panel should not be visible. 45 * @property {string} READY 46 * Ready to be activated. 47 * 48 * Note: If you update this list of states, make sure to update the 49 * corresponding documentation in the `docs` folder as well. 50 */ 51 export const IPProtectionStates = Object.freeze({ 52 UNINITIALIZED: "uninitialized", 53 UNAVAILABLE: "unavailable", 54 UNAUTHENTICATED: "unauthenticated", 55 OPTED_OUT: "optedout", 56 READY: "ready", 57 }); 58 59 /** 60 * A singleton service that manages proxy integration and backend functionality. 61 * 62 * @fires IPProtectionServiceSingleton#"IPProtectionService:StateChanged" 63 * When the proxy state machine changes state. Check the `state` attribute to 64 * know the current state. 65 */ 66 class IPProtectionServiceSingleton extends EventTarget { 67 #state = IPProtectionStates.UNINITIALIZED; 68 69 guardian = null; 70 71 #helpers = null; 72 73 /** 74 * Returns the state of the service. See the description of the state 75 * machine. 76 * 77 * @returns {string} - the current state from IPProtectionStates. 78 */ 79 get state() { 80 return this.#state; 81 } 82 83 constructor() { 84 super(); 85 86 this.guardian = new lazy.GuardianClient(); 87 88 this.updateState = this.#updateState.bind(this); 89 this.setState = this.#setState.bind(this); 90 91 this.#helpers = lazy.IPPHelpers; 92 } 93 94 /** 95 * Setups the IPProtectionService if enabled early during the firefox startup 96 * phases. 97 */ 98 async maybeEarlyInit() { 99 if ( 100 this.featureEnabled && 101 Services.prefs.getBoolPref("browser.ipProtection.autoStartEnabled") 102 ) { 103 await this.init(); 104 } 105 } 106 107 /** 108 * Setups the IPProtectionService if enabled. 109 */ 110 async init() { 111 if ( 112 this.#state !== IPProtectionStates.UNINITIALIZED || 113 !this.featureEnabled 114 ) { 115 return; 116 } 117 118 this.#helpers.forEach(helper => helper.init()); 119 120 this.#updateState(); 121 122 if (lazy.IPPStartupCache.isStartupCompleted) { 123 this.initOnStartupCompleted(); 124 } 125 } 126 127 /** 128 * Removes the UI widget. 129 */ 130 uninit() { 131 if (this.#state === IPProtectionStates.UNINITIALIZED) { 132 return; 133 } 134 135 this.#helpers.forEach(helper => helper.uninit()); 136 137 this.#setState(IPProtectionStates.UNINITIALIZED); 138 } 139 140 async initOnStartupCompleted() { 141 await Promise.allSettled( 142 this.#helpers.map(helper => helper.initOnStartupCompleted()) 143 ); 144 } 145 146 async startLoginFlow(browser) { 147 return lazy.SpecialMessageActions.fxaSignInFlow(SIGNIN_DATA, browser); 148 } 149 150 /** 151 * Recomputes the current state synchronously using the latest helper data. 152 * Callers should update their own inputs before invoking this. 153 */ 154 #updateState() { 155 this.#setState(this.#computeState()); 156 } 157 158 /** 159 * Checks observed statuses or with Guardian to get the current state. 160 * 161 * @returns {Promise<IPProtectionStates>} 162 */ 163 #computeState() { 164 // The IPP feature is disabled. 165 if (!this.featureEnabled) { 166 return IPProtectionStates.UNINITIALIZED; 167 } 168 169 if (lazy.IPPOptOutHelper.optedOut) { 170 return IPProtectionStates.OPTED_OUT; 171 } 172 173 // Maybe we have to use the cached state, because we are not initialized yet. 174 if (!lazy.IPPStartupCache.isStartupCompleted) { 175 return lazy.IPPStartupCache.state; 176 } 177 178 // If the VPN add-on is installed... 179 if ( 180 lazy.IPPVPNAddonHelper.vpnAddonDetected && 181 lazy.IPPEnrollAndEntitleManager.hasUpgraded 182 ) { 183 return IPProtectionStates.UNAVAILABLE; 184 } 185 186 // For non authenticated users, we can check if they are eligible (the UI 187 // is shown and they have to login) or we don't know yet their current 188 // enroll state (no UI is shown). 189 let eligible = lazy.IPPNimbusHelper.isEligible; 190 if (!lazy.IPPSignInWatcher.isSignedIn) { 191 return !eligible 192 ? IPProtectionStates.UNAVAILABLE 193 : IPProtectionStates.UNAUTHENTICATED; 194 } 195 196 // Check if the current account is enrolled and has an entitlement. 197 if (!lazy.IPPEnrollAndEntitleManager.isEnrolledAndEntitled && !eligible) { 198 return IPProtectionStates.UNAVAILABLE; 199 } 200 201 // The proxy can be activated. 202 return IPProtectionStates.READY; 203 } 204 205 /** 206 * Sets the current state and triggers the state change event if needed. 207 * 208 * @param {IPProtectionStates} newState 209 */ 210 #setState(newState) { 211 if (newState === this.#state) { 212 return; 213 } 214 215 let prevState = this.#state; 216 this.#state = newState; 217 218 this.#stateChanged(newState, prevState); 219 } 220 221 /** 222 * Handles side effects of a state change and dispatches the StateChanged event. 223 * 224 * @param {IPProtectionStates} state 225 * @param {IPProtectionStates} prevState 226 */ 227 #stateChanged(state, prevState) { 228 this.dispatchEvent( 229 new CustomEvent("IPProtectionService:StateChanged", { 230 bubbles: true, 231 composed: true, 232 detail: { 233 state, 234 prevState, 235 }, 236 }) 237 ); 238 } 239 } 240 241 const IPProtectionService = new IPProtectionServiceSingleton(); 242 243 XPCOMUtils.defineLazyPreferenceGetter( 244 IPProtectionService, 245 "featureEnabled", 246 ENABLED_PREF, 247 false, 248 (_pref, _oldVal, featureEnabled) => { 249 if (featureEnabled) { 250 IPProtectionService.init(); 251 } else { 252 IPProtectionService.uninit(); 253 } 254 } 255 ); 256 257 export { IPProtectionService };