forked from mirrors/gecko-dev
The DoHConfig now listens for region change notification and saves the new region in a pref. On the next initialization of the DoH config we will use the saved pref to update the doh region even if the pref was already set. This should also work for testing (manually setting the value of the pref) as long as no region changes happen. Differential Revision: https://phabricator.services.mozilla.com/D195023
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(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);
|
|
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();
|