forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			342 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			342 lines
		
	
	
	
		
			9.8 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 => {
 | 
						|
      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(sub, top, data) {
 | 
						|
        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);
 | 
						|
 | 
						|
    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);
 | 
						|
 | 
						|
    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;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  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();
 |