Bug 1545242 - Add DNS-over-HTTPS resolver picker to the connections prefs UI. r=flod,johannh

* Create new network.trr.resolvers pref which is a JSON array of objects with a name and url representing each resolver
* Add menulist to represent the resolver choices, and a "custom" option to use the network.trr.custom_uri as the trr.uri value

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Sam Foster 2019-05-03 16:15:45 +00:00
Родитель 28c31b907a
Коммит a0bed7fa3e
5 изменённых файлов: 256 добавлений и 69 удалений

Просмотреть файл

@ -37,6 +37,7 @@ Preferences.addAll([
{ id: "network.proxy.backup.socks_port", type: "int" },
{ id: "network.trr.mode", type: "int" },
{ id: "network.trr.uri", type: "string" },
{ id: "network.trr.resolvers", type: "string" },
{ id: "network.trr.custom_uri", "type": "string" },
]);
@ -46,11 +47,21 @@ window.addEventListener("DOMContentLoaded", () => {
Preferences.get("network.proxy.socks_version").on("change",
gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog));
// wait until the network.trr prefs are added before init'ing the UI for them
Preferences.get("network.trr.uri").on("change", () => {
gConnectionsDialog.updateDnsOverHttpsUI();
});
Preferences.get("network.trr.resolvers").on("change", () => {
gConnectionsDialog.initDnsOverHttpsUI();
});
// XXX: We can't init the DNS-over-HTTPs UI until the onsyncfrompreference for network.trr.mode
// has been called. The uiReady promise will be resolved after the first call to
// readDnsOverHttpsMode and the subsequent call to initDnsOverHttpsUI has happened.
gConnectionsDialog.uiReady = new Promise(resolve => {
gConnectionsDialog._initialPrefsAdded = resolve;
gConnectionsDialog._areTrrPrefsReady = false;
gConnectionsDialog._handleTrrPrefsReady = resolve;
}).then(() => {
delete gConnectionsDialog._initialPrefsAdded;
gConnectionsDialog.initDnsOverHttpsUI();
});
@ -65,8 +76,16 @@ window.addEventListener("DOMContentLoaded", () => {
var gConnectionsDialog = {
beforeAccept() {
if (document.getElementById("customDnsOverHttpsUrlRadio").selected) {
Services.prefs.setStringPref("network.trr.uri", document.getElementById("customDnsOverHttpsInput").value);
let dnsOverHttpsResolverChoice = document.getElementById("networkDnsOverHttpsResolverChoices").value;
if (dnsOverHttpsResolverChoice == "custom") {
let customValue = document.getElementById("networkCustomDnsOverHttpsInput").value.trim();
if (customValue) {
Services.prefs.setStringPref("network.trr.uri", customValue);
} else {
Services.prefs.clearUserPref("network.trr.uri");
}
} else {
Services.prefs.setStringPref("network.trr.uri", dnsOverHttpsResolverChoice);
}
var proxyTypePref = Preferences.get("network.proxy.type");
@ -296,6 +315,31 @@ var gConnectionsDialog = {
}
},
get dnsOverHttpsResolvers() {
let rawValue = Preferences.get("network.trr.resolvers", "").value;
// if there's no default, we'll hold its position with an empty string
let defaultURI = Preferences.get("network.trr.uri", "").defaultValue;
let providers = [];
if (rawValue) {
try {
providers = JSON.parse(rawValue);
} catch (ex) {
Cu.reportError(`Bad JSON data in pref network.trr.resolvers: ${rawValue}`);
}
}
if (!Array.isArray(providers)) {
Cu.reportError(`Expected a JSON array in network.trr.resolvers: ${rawValue}`);
providers = [];
}
let defaultIndex = providers.findIndex(p => p.url == defaultURI);
if (defaultIndex == -1 && defaultURI) {
// the default value for the pref isn't included in the resolvers list
// so we'll make a stub for it. Without an id, we'll have to use the url as the label
providers.unshift({ url: defaultURI });
}
return providers;
},
isDnsOverHttpsLocked() {
return Services.prefs.prefIsLocked("network.trr.mode");
},
@ -309,12 +353,16 @@ var gConnectionsDialog = {
readDnsOverHttpsMode() {
// called to update checked element property to reflect current pref value
// this is the first signal we get when the prefs are added, so lazy-init
let enabled = this.isDnsOverHttpsEnabled();
let uriPref = Preferences.get("network.trr.uri");
uriPref.disabled = !enabled || this.isDnsOverHttpsLocked();
if (this._initialPrefsAdded) {
this._initialPrefsAdded();
// this is the first signal we get when the prefs are available, so
// lazy-init if appropriate
if (!this._areTrrPrefsReady) {
this._areTrrPrefsReady = true;
this._handleTrrPrefsReady();
} else {
this.updateDnsOverHttpsUI();
}
return enabled;
},
@ -327,42 +375,91 @@ var gConnectionsDialog = {
},
updateDnsOverHttpsUI() {
// Disable the custom url input box if the parent checkbox and custom radio button attached to it is not selected.
// Disable the custom radio button if the parent checkbox is not selected.
let parentCheckbox = document.getElementById("networkDnsOverHttps");
let customDnsOverHttpsUrlRadio = document.getElementById("customDnsOverHttpsUrlRadio");
let customDnsOverHttpsInput = document.getElementById("customDnsOverHttpsInput");
customDnsOverHttpsInput.disabled = !parentCheckbox.checked || !customDnsOverHttpsUrlRadio.selected;
customDnsOverHttpsUrlRadio.disabled = !parentCheckbox.checked;
// init and update of the UI must wait until the pref values are ready
if (!this._areTrrPrefsReady) {
return;
}
let [menu, customInput] = this.getDnsOverHttpsControls();
let customContainer = document.getElementById("customDnsOverHttpsContainer");
let customURI = Preferences.get("network.trr.custom_uri").value;
let currentURI = Preferences.get("network.trr.uri").value;
let resolvers = this.dnsOverHttpsResolvers;
let isCustom = menu.value == "custom";
if (this.isDnsOverHttpsEnabled()) {
this.toggleDnsOverHttpsUI(false);
if (isCustom) {
// if the current and custom_uri values mismatch, update the uri pref
if (currentURI && !customURI && !resolvers.find(r => r.url == currentURI)) {
Services.prefs.setStringPref("network.trr.custom_uri", currentURI);
}
}
} else {
this.toggleDnsOverHttpsUI(true);
}
if (!menu.disabled && isCustom) {
customContainer.hidden = false;
customInput.disabled = false;
customContainer.scrollIntoView();
} else {
customContainer.hidden = true;
customInput.disabled = true;
}
},
getDnsOverHttpsControls() {
return [
document.getElementById("networkDnsOverHttps"),
document.getElementById("customDnsOverHttpsUrlRadio"),
document.getElementById("defaultDnsOverHttpsUrlRadio"),
document.getElementById("customDnsOverHttpsInput"),
document.getElementById("networkDnsOverHttpsResolverChoices"),
document.getElementById("networkCustomDnsOverHttpsInput"),
document.getElementById("networkDnsOverHttpsResolverChoicesLabel"),
document.getElementById("networkCustomDnsOverHttpsInputLabel"),
];
},
disableDnsOverHttpsUI(disabled) {
toggleDnsOverHttpsUI(disabled) {
for (let element of this.getDnsOverHttpsControls()) {
element.disabled = disabled;
}
},
initDnsOverHttpsUI() {
// If we have a locked pref disable the UI.
this.disableDnsOverHttpsUI(this.isDnsOverHttpsLocked());
let resolvers = this.dnsOverHttpsResolvers;
let defaultURI = Preferences.get("network.trr.uri").defaultValue;
let currentURI = Preferences.get("network.trr.uri").value;
let menu = document.getElementById("networkDnsOverHttpsResolverChoices");
let defaultDnsOverHttpsUrlRadio = document.getElementById("defaultDnsOverHttpsUrlRadio");
let defaultPrefUrl = Preferences.get("network.trr.uri").defaultValue;
document.l10n.setAttributes(defaultDnsOverHttpsUrlRadio, "connection-dns-over-https-url-default", {
url: defaultPrefUrl,
});
defaultDnsOverHttpsUrlRadio.value = defaultPrefUrl;
let radioGroup = document.getElementById("DnsOverHttpsUrlRadioGroup");
radioGroup.selectedIndex = Preferences.get("network.trr.uri").hasUserValue ? 1 : 0;
this.updateDnsOverHttpsUI();
// populate the DNS-Over-HTTPs resolver list
menu.removeAllItems();
for (let resolver of resolvers) {
let item = menu.appendItem(undefined, resolver.url);
if (resolver.url == defaultURI) {
document.l10n.setAttributes(item, "connection-dns-over-https-url-item-default", {
name: resolver.name || resolver.url,
});
} else {
item.label = resolver.name || resolver.url;
}
}
let lastItem = menu.appendItem(undefined, "custom");
document.l10n.setAttributes(lastItem, "connection-dns-over-https-url-custom");
// set initial selection in the resolver provider picker
let selectedIndex = currentURI ? resolvers.findIndex(r => r.url == currentURI) : 0;
if (selectedIndex == -1) {
// select the last "Custom" item
selectedIndex = menu.itemCount - 1;
}
menu.selectedIndex = selectedIndex;
if (this.isDnsOverHttpsLocked()) {
// disable all the options and the checkbox itself to disallow enabling them
this.toggleDnsOverHttpsUI(true);
document.getElementById("networkDnsOverHttps").disabled = true;
} else {
this.toggleDnsOverHttpsUI(false);
this.updateDnsOverHttpsUI();
document.getElementById("networkDnsOverHttps").disabled = false;
}
},
};

Просмотреть файл

@ -148,25 +148,39 @@
<checkbox id="networkProxySOCKSRemoteDNS"
preference="network.proxy.socks_remote_dns"
data-l10n-id="connection-proxy-socks-remote-dns" />
<checkbox id="networkDnsOverHttps"
<groupbox>
<checkbox id="networkDnsOverHttps"
data-l10n-id="connection-dns-over-https"
preference="network.trr.mode"
onsyncfrompreference="return gConnectionsDialog.readDnsOverHttpsMode();"
onsynctopreference="return gConnectionsDialog.writeDnsOverHttpsMode();"
oncommand="gConnectionsDialog.updateDnsOverHttpsUI();"/>
<vbox class="indent" flex="1">
<radiogroup id="DnsOverHttpsUrlRadioGroup" orient="vertical">
<radio id="defaultDnsOverHttpsUrlRadio"
data-l10n-id="connection-dns-over-https-url-default"
data-l10n-args='{"url":""}'
preference="network.trr.uri"
oncommand="gConnectionsDialog.updateDnsOverHttpsUI();"/>
<hbox>
<radio id="customDnsOverHttpsUrlRadio"
data-l10n-id="connection-dns-over-https-url-custom"
oncommand="gConnectionsDialog.updateDnsOverHttpsUI();"/>
<textbox id="customDnsOverHttpsInput" flex="1" preference="network.trr.custom_uri"/>
</hbox>
</radiogroup>
</vbox>
onsynctopreference="return gConnectionsDialog.writeDnsOverHttpsMode();"/>
<grid class="indent" flex="1">
<columns>
<column></column>
<column flex="1"></column>
</columns>
<rows>
<row align="center">
<hbox pack="end">
<label id="networkDnsOverHttpsResolverChoicesLabel"
data-l10n-id="connection-dns-over-https-url-resolver"
control="networkDnsOverHttpsResolverChoices"/>
</hbox>
<menulist id="networkDnsOverHttpsResolverChoices"
oncommand="gConnectionsDialog.updateDnsOverHttpsUI()"></menulist>
</row>
<row align="center" id="customDnsOverHttpsContainer" hidden="true">
<hbox pack="end">
<label id="networkCustomDnsOverHttpsInputLabel"
data-l10n-id="connection-dns-over-https-custom-label"
control="networkCustomDnsOverHttpsInput"/>
</hbox>
<textbox id="networkCustomDnsOverHttpsInput" flex="1"
preference="network.trr.custom_uri"/>
</row>
</rows>
</grid>
</groupbox>
</dialog>

Просмотреть файл

@ -3,19 +3,27 @@ var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const SUBDIALOG_URL = "chrome://browser/content/preferences/connection.xul";
const TRR_MODE_PREF = "network.trr.mode";
const TRR_URI_PREF = "network.trr.uri";
const TRR_RESOLVERS_PREF = "network.trr.resolvers";
const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri";
const DEFAULT_RESOLVER_VALUE = "https://mozilla.cloudflare-dns.com/dns-query";
const modeCheckboxSelector = "#networkDnsOverHttps";
const uriTextboxSelector = "#customDnsOverHttpsInput";
const uriTextboxSelector = "#networkCustomDnsOverHttpsInput";
const resolverMenulistSelector = "#networkDnsOverHttpsResolverChoices";
const defaultPrefValues = Object.freeze({
[TRR_MODE_PREF]: 0,
[TRR_URI_PREF]: "https://mozilla.cloudflare-dns.com/dns-query",
[TRR_RESOLVERS_PREF]: JSON.stringify([
{ "name": "Cloudflare", "url": DEFAULT_RESOLVER_VALUE },
{ "name": "example.org", "url": "https://example.org/dns-query" },
]),
[TRR_CUSTOM_URI_PREF]: "",
});
function resetPrefs() {
Services.prefs.clearUserPref(TRR_MODE_PREF);
Services.prefs.clearUserPref(TRR_URI_PREF);
Services.prefs.clearUserPref(TRR_RESOLVERS_PREF);
Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF);
}
@ -62,6 +70,10 @@ async function testWithProperties(props, startTime) {
if (props.hasOwnProperty(TRR_URI_PREF)) {
Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]);
}
if (props.hasOwnProperty(TRR_RESOLVERS_PREF)) {
info(`Setting ${TRR_RESOLVERS_PREF} to ${props[TRR_RESOLVERS_PREF]}`);
Services.prefs.setStringPref(TRR_RESOLVERS_PREF, props[TRR_RESOLVERS_PREF]);
}
let dialog = await openConnectionsSubDialog();
await dialog.uiReady;
@ -72,6 +84,7 @@ async function testWithProperties(props, startTime) {
"dialogclosing");
let modeCheckbox = doc.querySelector(modeCheckboxSelector);
let uriTextbox = doc.querySelector(uriTextboxSelector);
let resolverMenulist = doc.querySelector(resolverMenulistSelector);
let uriPrefChangedPromise;
let modePrefChangedPromise;
@ -81,6 +94,9 @@ async function testWithProperties(props, startTime) {
if (props.hasOwnProperty("expectedUriValue")) {
is(uriTextbox.value, props.expectedUriValue, "URI textbox has expected value");
}
if (props.hasOwnProperty("expectedResolverListValue")) {
is(resolverMenulist.value, props.expectedResolverListValue, "resolver menulist has expected value");
}
if (props.clickMode) {
info((Date.now() - startTime) + ": testWithProperties: clickMode, waiting for the pref observer");
modePrefChangedPromise = waitForPrefObserver(TRR_MODE_PREF);
@ -89,6 +105,15 @@ async function testWithProperties(props, startTime) {
EventUtils.synthesizeMouseAtCenter(modeCheckbox, {}, win);
info((Date.now() - startTime) + ": testWithProperties: clickMode, mouse click synthesized");
}
if (props.hasOwnProperty("selectResolver")) {
info((Date.now() - startTime) + ": testWithProperties: selectResolver, creating change event");
resolverMenulist.focus();
resolverMenulist.value = props.selectResolver;
resolverMenulist.dispatchEvent(new Event("input", {bubbles: true}));
resolverMenulist.dispatchEvent(new Event("change", {bubbles: true}));
info((Date.now() - startTime) + ": testWithProperties: selectResolver, item value set and events dispatched");
}
if (props.hasOwnProperty("inputUriKeys")) {
info((Date.now() - startTime) + ": testWithProperties: inputUriKeys, waiting for the pref observer");
uriPrefChangedPromise = waitForPrefObserver(TRR_CUSTOM_URI_PREF);
@ -120,6 +145,12 @@ async function testWithProperties(props, startTime) {
let modePref = Services.prefs.getIntPref(TRR_MODE_PREF);
is(modePref, props.expectedModePref, "mode pref ended up with the expected value");
}
if (props.hasOwnProperty("expectedFinalCusomUriPref")) {
let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF);
is(customUriPref, props.expectedFinalCustomUriPref, "custom_uri pref ended up with the expected value");
}
info((Date.now() - startTime) + ": testWithProperties: fin");
}
@ -137,40 +168,78 @@ add_task(async function default_values() {
let testVariations = [
// verify state with defaults
{ expectedModePref: 0, expectedUriValue: "" },
{ name: "default", expectedModePref: 0, expectedUriValue: "" },
// verify each of the modes maps to the correct checked state
{ [TRR_MODE_PREF]: 0, expectedModeChecked: false },
{ [TRR_MODE_PREF]: 1, expectedModeChecked: true },
{ [TRR_MODE_PREF]: 2, expectedModeChecked: true },
{ [TRR_MODE_PREF]: 3, expectedModeChecked: true },
{ [TRR_MODE_PREF]: 4, expectedModeChecked: true },
{ [TRR_MODE_PREF]: 5, expectedModeChecked: false },
{ name: "mode 0",
[TRR_MODE_PREF]: 0, expectedModeChecked: false },
{ name: "mode 1",
[TRR_MODE_PREF]: 1, expectedModeChecked: true, expectedFinalUriPref: DEFAULT_RESOLVER_VALUE},
{ name: "mode 2",
[TRR_MODE_PREF]: 2, expectedModeChecked: true, expectedFinalUriPref: DEFAULT_RESOLVER_VALUE},
{ name: "mode 3",
[TRR_MODE_PREF]: 3, expectedModeChecked: true, expectedFinalUriPref: DEFAULT_RESOLVER_VALUE},
{ name: "mode 4",
[TRR_MODE_PREF]: 4, expectedModeChecked: true, expectedFinalUriPref: DEFAULT_RESOLVER_VALUE},
{ name: "mode 5",
[TRR_MODE_PREF]: 5, expectedModeChecked: false },
// verify an out of bounds mode value maps to the correct checked state
{ [TRR_MODE_PREF]: 77, expectedModeChecked: false },
{ name: "mode out-of-bounds",
[TRR_MODE_PREF]: 77, expectedModeChecked: false },
// verify toggling the checkbox gives the right outcomes
{ clickMode: true, expectedModeValue: 2, expectedUriValue: "" },
{ name: "toggle mode on",
clickMode: true, expectedModeValue: 2, expectedUriValue: "", expectedFinalUriPref: DEFAULT_RESOLVER_VALUE },
{
name: "toggle mode off",
[TRR_MODE_PREF]: 4,
expectedModeChecked: true, clickMode: true, expectedModePref: 0,
},
// test that setting TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF
// test that selecting Custom, when we have a TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF
{
name: "select custom with existing custom_uri pref value",
[TRR_MODE_PREF]: 2, [TRR_CUSTOM_URI_PREF]: "https://example.com",
expectedModeValue: true, expectedUriValue: "https://example.com",
expectedModeValue: true, selectResolver: "custom", expectedUriValue: "https://example.com",
expectedFinalUriPref: "https://example.com",
expectedFinalCustomUriPref: "https://example.com",
},
{
[TRR_URI_PREF]: "",
clickMode: true, inputUriKeys: "https://example.com",
name: "select custom and enter new custom_uri pref value",
[TRR_URI_PREF]: "", [TRR_CUSTOM_URI_PREF]: "",
clickMode: true, selectResolver: "custom", inputUriKeys: "https://example.com",
expectedModePref: 2, expectedFinalUriPref: "https://example.com",
expectedFinalCustomUriPref: "https://example.com",
},
// verify the uri can be cleared
{
name: "return to default from custom",
[TRR_MODE_PREF]: 2, [TRR_URI_PREF]: "https://example.com", [TRR_CUSTOM_URI_PREF]: "https://example.com",
expectedUriValue: "https://example.com", inputUriKeys: "", expectedFinalUriPref: "",
expectedUriValue: "https://example.com",
expectedResolverListValue: "custom",
selectResolver: DEFAULT_RESOLVER_VALUE,
expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
expectedFinalCustomUriPref: "https://example.com",
},
{
name: "clear the custom uri",
[TRR_MODE_PREF]: 2, [TRR_URI_PREF]: "https://example.com", [TRR_CUSTOM_URI_PREF]: "https://example.com",
expectedUriValue: "https://example.com",
expectedResolverListValue: "custom",
inputUriKeys: "",
expectedFinalUriPref: DEFAULT_RESOLVER_VALUE,
expectedFinalCustomUriPref: "",
},
{
name: "empty default resolver list",
[TRR_RESOLVERS_PREF]: "",
[TRR_MODE_PREF]: 2, [TRR_URI_PREF]: "https://example.com", [TRR_CUSTOM_URI_PREF]: "",
[TRR_RESOLVERS_PREF]: "",
expectedUriValue: "https://example.com",
expectedResolverListValue: "custom",
expectedFinalUriPref: "https://example.com",
expectedFinalCustomUriPref: "https://example.com",
},
];
for (let props of testVariations) {
@ -178,6 +247,7 @@ for (let props of testVariations) {
await preferencesOpen;
let startTime = Date.now();
resetPrefs();
info("starting test: " + props.name);
await testWithProperties(props, startTime);
});
}

Просмотреть файл

@ -86,14 +86,18 @@ connection-dns-over-https =
.label = Enable DNS over HTTPS
.accesskey = b
connection-dns-over-https-url-resolver = Use Provider
.accesskey = P
# Variables:
# $url (String) - URL for the DNS over HTTPS provider
connection-dns-over-https-url-default =
.label = Use default ({ $url })
.accesskey = U
# $name (String) - Display name or URL for the DNS over HTTPS provider
connection-dns-over-https-url-item-default =
.label = { $name } (Default)
.tooltiptext = Use the default URL for resolving DNS over HTTPS
connection-dns-over-https-url-custom =
.label = Custom
.accesskey = C
.tooltiptext = Enter your preferred URL for resolving DNS over HTTPS
connection-dns-over-https-custom-label = Custom

Просмотреть файл

@ -5515,6 +5515,8 @@ pref("network.connectivity-service.IPv6.url", "http://detectportal.firefox.com/s
pref("network.trr.mode", 0);
// DNS-over-HTTP service to use, must be HTTPS://
pref("network.trr.uri", "https://mozilla.cloudflare-dns.com/dns-query");
// DNS-over-HTTP service options, must be HTTPS://
pref("network.trr.resolvers", "[{ \"name\": \"Cloudflare\", \"url\": \"https://mozilla.cloudflare-dns.com/dns-query\" }]");
// credentials to pass to DOH end-point
pref("network.trr.credentials", "");
pref("network.trr.custom_uri", "");