forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			356 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| /*
 | |
|  * This module provides an interface to access DoH configuration - e.g. whether
 | |
|  * DoH is enabled, whether capabilities are enabled, etc. The configuration is
 | |
|  * sourced from either Remote Settings or pref values, with Remote Settings
 | |
|  * being preferred.
 | |
|  */
 | |
| 
 | |
| import { RemoteSettings } from "resource://services-settings/remote-settings.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   Preferences: "resource://gre/modules/Preferences.sys.mjs",
 | |
|   Region: "resource://gre/modules/Region.sys.mjs",
 | |
| });
 | |
| 
 | |
| const kGlobalPrefBranch = "doh-rollout";
 | |
| var kRegionPrefBranch;
 | |
| 
 | |
| const kConfigPrefs = {
 | |
|   kEnabledPref: "enabled",
 | |
|   kProvidersPref: "provider-list",
 | |
|   kTRRSelectionEnabledPref: "trr-selection.enabled",
 | |
|   kTRRSelectionProvidersPref: "trr-selection.provider-list",
 | |
|   kTRRSelectionCommitResultPref: "trr-selection.commit-result",
 | |
|   kProviderSteeringEnabledPref: "provider-steering.enabled",
 | |
|   kProviderSteeringListPref: "provider-steering.provider-list",
 | |
| };
 | |
| 
 | |
| const kPrefChangedTopic = "nsPref:changed";
 | |
| 
 | |
| const gProvidersCollection = RemoteSettings("doh-providers");
 | |
| const gConfigCollection = RemoteSettings("doh-config");
 | |
| 
 | |
| function getPrefValueRegionFirst(prefName) {
 | |
|   let regionalPrefName = `${kRegionPrefBranch}.${prefName}`;
 | |
|   let regionalPrefValue = lazy.Preferences.get(regionalPrefName);
 | |
|   if (regionalPrefValue !== undefined) {
 | |
|     return regionalPrefValue;
 | |
|   }
 | |
|   return lazy.Preferences.get(`${kGlobalPrefBranch}.${prefName}`);
 | |
| }
 | |
| 
 | |
| function getProviderListFromPref(prefName) {
 | |
|   let prefVal = getPrefValueRegionFirst(prefName);
 | |
|   if (prefVal) {
 | |
|     try {
 | |
|       return JSON.parse(prefVal);
 | |
|     } catch (e) {
 | |
|       console.error(`DoH provider list not a valid JSON array: ${prefName}`);
 | |
|     }
 | |
|   }
 | |
|   return undefined;
 | |
| }
 | |
| 
 | |
| // Generate a base config object with getters that return pref values. When
 | |
| // Remote Settings values become available, a new config object will be
 | |
| // generated from this and specific fields will be replaced by the RS value.
 | |
| // If we use a class to store base config and instantiate new config objects
 | |
| // from it, we lose the ability to override getters because they are defined
 | |
| // as non-configureable properties on class instances. So just use a function.
 | |
| function makeBaseConfigObject() {
 | |
|   function makeConfigProperty({
 | |
|     obj,
 | |
|     propName,
 | |
|     defaultVal,
 | |
|     prefName,
 | |
|     isProviderList,
 | |
|   }) {
 | |
|     let prefFn = isProviderList
 | |
|       ? getProviderListFromPref
 | |
|       : getPrefValueRegionFirst;
 | |
| 
 | |
|     let overridePropName = "_" + propName;
 | |
| 
 | |
|     Object.defineProperty(obj, propName, {
 | |
|       get() {
 | |
|         // If a pref value exists, it gets top priority. Otherwise, if it has an
 | |
|         // explicitly set value (from Remote Settings), we return that.
 | |
|         let prefVal = prefFn(prefName);
 | |
|         if (prefVal !== undefined) {
 | |
|           return prefVal;
 | |
|         }
 | |
|         if (this[overridePropName] !== undefined) {
 | |
|           return this[overridePropName];
 | |
|         }
 | |
|         return defaultVal;
 | |
|       },
 | |
|       set(val) {
 | |
|         this[overridePropName] = val;
 | |
|       },
 | |
|     });
 | |
|   }
 | |
|   let newConfig = {
 | |
|     get fallbackProviderURI() {
 | |
|       return this.providerList[0]?.uri;
 | |
|     },
 | |
|     trrSelection: {},
 | |
|     providerSteering: {},
 | |
|   };
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig,
 | |
|     propName: "enabled",
 | |
|     defaultVal: false,
 | |
|     prefName: kConfigPrefs.kEnabledPref,
 | |
|     isProviderList: false,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig,
 | |
|     propName: "providerList",
 | |
|     defaultVal: [],
 | |
|     prefName: kConfigPrefs.kProvidersPref,
 | |
|     isProviderList: true,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig.trrSelection,
 | |
|     propName: "enabled",
 | |
|     defaultVal: false,
 | |
|     prefName: kConfigPrefs.kTRRSelectionEnabledPref,
 | |
|     isProviderList: false,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig.trrSelection,
 | |
|     propName: "commitResult",
 | |
|     defaultVal: false,
 | |
|     prefName: kConfigPrefs.kTRRSelectionCommitResultPref,
 | |
|     isProviderList: false,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig.trrSelection,
 | |
|     propName: "providerList",
 | |
|     defaultVal: [],
 | |
|     prefName: kConfigPrefs.kTRRSelectionProvidersPref,
 | |
|     isProviderList: true,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig.providerSteering,
 | |
|     propName: "enabled",
 | |
|     defaultVal: false,
 | |
|     prefName: kConfigPrefs.kProviderSteeringEnabledPref,
 | |
|     isProviderList: false,
 | |
|   });
 | |
|   makeConfigProperty({
 | |
|     obj: newConfig.providerSteering,
 | |
|     propName: "providerList",
 | |
|     defaultVal: [],
 | |
|     prefName: kConfigPrefs.kProviderSteeringListPref,
 | |
|     isProviderList: true,
 | |
|   });
 | |
|   return newConfig;
 | |
| }
 | |
| 
 | |
| export const DoHConfigController = {
 | |
|   initComplete: null,
 | |
|   _resolveInitComplete: null,
 | |
| 
 | |
|   // This field always contains the current config state, for
 | |
|   // consumer use.
 | |
|   currentConfig: makeBaseConfigObject(),
 | |
| 
 | |
|   // Loads the client's region via Region.sys.mjs. This might mean waiting
 | |
|   // until the region is available.
 | |
|   async loadRegion() {
 | |
|     await new Promise(resolve => {
 | |
|       // If the region has changed since it was last set, update the pref.
 | |
|       let homeRegionChanged = lazy.Preferences.get(
 | |
|         `${kGlobalPrefBranch}.home-region-changed`
 | |
|       );
 | |
|       if (homeRegionChanged) {
 | |
|         lazy.Preferences.reset(`${kGlobalPrefBranch}.home-region-changed`);
 | |
|         lazy.Preferences.reset(`${kGlobalPrefBranch}.home-region`);
 | |
|       }
 | |
| 
 | |
|       let homeRegion = lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`);
 | |
|       if (homeRegion) {
 | |
|         kRegionPrefBranch = `${kGlobalPrefBranch}.${homeRegion.toLowerCase()}`;
 | |
|         resolve();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let updateRegionAndResolve = () => {
 | |
|         kRegionPrefBranch = `${kGlobalPrefBranch}.${lazy.Region.home.toLowerCase()}`;
 | |
|         lazy.Preferences.set(
 | |
|           `${kGlobalPrefBranch}.home-region`,
 | |
|           lazy.Region.home
 | |
|         );
 | |
|         resolve();
 | |
|       };
 | |
| 
 | |
|       if (lazy.Region.home) {
 | |
|         updateRegionAndResolve();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       Services.obs.addObserver(function obs() {
 | |
|         Services.obs.removeObserver(obs, lazy.Region.REGION_TOPIC);
 | |
|         updateRegionAndResolve();
 | |
|       }, lazy.Region.REGION_TOPIC);
 | |
|     });
 | |
| 
 | |
|     // Finally, reload config.
 | |
|     await this.updateFromRemoteSettings();
 | |
|   },
 | |
| 
 | |
|   async init() {
 | |
|     await this.loadRegion();
 | |
| 
 | |
|     Services.prefs.addObserver(`${kGlobalPrefBranch}.`, this, true);
 | |
|     Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
 | |
| 
 | |
|     gProvidersCollection.on("sync", this.updateFromRemoteSettings);
 | |
|     gConfigCollection.on("sync", this.updateFromRemoteSettings);
 | |
| 
 | |
|     this._resolveInitComplete();
 | |
|   },
 | |
| 
 | |
|   // Useful for tests to set prior state before init()
 | |
|   async _uninit() {
 | |
|     await this.initComplete;
 | |
| 
 | |
|     Services.prefs.removeObserver(`${kGlobalPrefBranch}`, this);
 | |
|     Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
 | |
| 
 | |
|     gProvidersCollection.off("sync", this.updateFromRemoteSettings);
 | |
|     gConfigCollection.off("sync", this.updateFromRemoteSettings);
 | |
| 
 | |
|     this.initComplete = new Promise(resolve => {
 | |
|       this._resolveInitComplete = resolve;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     switch (topic) {
 | |
|       case kPrefChangedTopic:
 | |
|         let allowedPrefs = Object.getOwnPropertyNames(kConfigPrefs).map(
 | |
|           k => kConfigPrefs[k]
 | |
|         );
 | |
|         if (
 | |
|           !allowedPrefs.some(pref =>
 | |
|             [
 | |
|               `${kRegionPrefBranch}.${pref}`,
 | |
|               `${kGlobalPrefBranch}.${pref}`,
 | |
|             ].includes(data)
 | |
|           )
 | |
|         ) {
 | |
|           break;
 | |
|         }
 | |
|         this.notifyNewConfig();
 | |
|         break;
 | |
|       case lazy.Region.REGION_TOPIC:
 | |
|         lazy.Preferences.set(`${kGlobalPrefBranch}.home-region-changed`, true);
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   QueryInterface: ChromeUtils.generateQI([
 | |
|     "nsIObserver",
 | |
|     "nsISupportsWeakReference",
 | |
|   ]),
 | |
| 
 | |
|   // Creates new config object from currently available
 | |
|   // Remote Settings values.
 | |
|   async updateFromRemoteSettings() {
 | |
|     let providers = await gProvidersCollection.get();
 | |
|     let config = await gConfigCollection.get();
 | |
| 
 | |
|     let providersById = new Map();
 | |
|     providers.forEach(p => providersById.set(p.id, p));
 | |
| 
 | |
|     let configByRegion = new Map();
 | |
|     config.forEach(c => {
 | |
|       c.id = c.id.toLowerCase();
 | |
|       configByRegion.set(c.id, c);
 | |
|     });
 | |
| 
 | |
|     let homeRegion = lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`);
 | |
|     let localConfig =
 | |
|       configByRegion.get(homeRegion?.toLowerCase()) ||
 | |
|       configByRegion.get("global");
 | |
| 
 | |
|     // Make a new config object first, mutate it as needed, then synchronously
 | |
|     // replace the currentConfig object at the end to ensure atomicity.
 | |
|     let newConfig = makeBaseConfigObject();
 | |
| 
 | |
|     if (!localConfig) {
 | |
|       DoHConfigController.currentConfig = newConfig;
 | |
|       DoHConfigController.notifyNewConfig();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (localConfig.rolloutEnabled) {
 | |
|       newConfig.enabled = true;
 | |
|     }
 | |
| 
 | |
|     let parseProviderList = (list, checkFn) => {
 | |
|       let parsedList = [];
 | |
|       list?.split(",")?.forEach(p => {
 | |
|         p = p.trim();
 | |
|         if (!p.length) {
 | |
|           return;
 | |
|         }
 | |
|         p = providersById.get(p);
 | |
|         if (!p || (checkFn && !checkFn(p))) {
 | |
|           return;
 | |
|         }
 | |
|         parsedList.push(p);
 | |
|       });
 | |
|       return parsedList;
 | |
|     };
 | |
| 
 | |
|     let regionalProviders = parseProviderList(localConfig.providers);
 | |
|     if (regionalProviders?.length) {
 | |
|       newConfig.providerList = regionalProviders;
 | |
|     }
 | |
| 
 | |
|     if (localConfig.steeringEnabled) {
 | |
|       let steeringProviders = parseProviderList(
 | |
|         localConfig.steeringProviders,
 | |
|         p => p.canonicalName?.length
 | |
|       );
 | |
|       if (steeringProviders?.length) {
 | |
|         newConfig.providerSteering.providerList = steeringProviders;
 | |
|         newConfig.providerSteering.enabled = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (localConfig.autoDefaultEnabled) {
 | |
|       let defaultProviders = parseProviderList(
 | |
|         localConfig.autoDefaultProviders
 | |
|       );
 | |
|       if (defaultProviders?.length) {
 | |
|         newConfig.trrSelection.providerList = defaultProviders;
 | |
|         newConfig.trrSelection.enabled = true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Finally, update the currentConfig object synchronously.
 | |
|     DoHConfigController.currentConfig = newConfig;
 | |
| 
 | |
|     DoHConfigController.notifyNewConfig();
 | |
|   },
 | |
| 
 | |
|   kConfigUpdateTopic: "doh-config-updated",
 | |
|   notifyNewConfig() {
 | |
|     Services.obs.notifyObservers(null, this.kConfigUpdateTopic);
 | |
|   },
 | |
| };
 | |
| 
 | |
| DoHConfigController.initComplete = new Promise(resolve => {
 | |
|   DoHConfigController._resolveInitComplete = resolve;
 | |
| });
 | |
| DoHConfigController.init();
 | 
