/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; requestLongerTimeout(4); const { EnterprisePolicyTesting, PoliciesPrefTracker, } = ChromeUtils.importESModule( "resource://testing-common/EnterprisePolicyTesting.sys.mjs" ); ChromeUtils.defineModuleGetter( this, "DoHController", "resource:///modules/DoHController.jsm" ); ChromeUtils.defineModuleGetter( this, "DoHConfigController", "resource:///modules/DoHConfig.jsm" ); ChromeUtils.defineModuleGetter( this, "DoHTestUtils", "resource://testing-common/DoHTestUtils.jsm" ); const SUBDIALOG_URL = "chrome://browser/content/preferences/dialogs/connection.xhtml"; const TRR_MODE_PREF = "network.trr.mode"; const TRR_URI_PREF = "network.trr.uri"; const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri"; const ROLLOUT_ENABLED_PREF = "doh-rollout.enabled"; const ROLLOUT_SELF_ENABLED_PREF = "doh-rollout.self-enabled"; const HEURISTICS_DISABLED_PREF = "doh-rollout.disable-heuristics"; const FIRST_RESOLVER_VALUE = DoHTestUtils.providers[0].uri; const SECOND_RESOLVER_VALUE = DoHTestUtils.providers[1].uri; const DEFAULT_RESOLVER_VALUE = FIRST_RESOLVER_VALUE; const modeCheckboxSelector = "#networkDnsOverHttps"; const uriTextboxSelector = "#networkCustomDnsOverHttpsInput"; const resolverMenulistSelector = "#networkDnsOverHttpsResolverChoices"; const defaultPrefValues = Object.freeze({ [TRR_MODE_PREF]: 0, [TRR_CUSTOM_URI_PREF]: "", }); // See bug 1741554. This test should not actually try to create a connection to // the real DoH endpoint. But a background request could do that while the test // is in progress, before we've actually disabled TRR, and would cause a crash // due to connecting to a non-local IP. // To prevent that we override the IP to a local address. Cc["@mozilla.org/network/native-dns-override;1"] .getService(Ci.nsINativeDNSResolverOverride) .addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1"); async function resetPrefs() { await DoHTestUtils.resetRemoteSettingsConfig(); await DoHController._uninit(); Services.prefs.clearUserPref(TRR_MODE_PREF); Services.prefs.clearUserPref(TRR_URI_PREF); Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF); Services.prefs.getChildList("doh-rollout.").forEach(pref => { Services.prefs.clearUserPref(pref); }); // Clear out any telemetry events generated by DoHController so that we don't // confuse tests running after this one that are looking at those. Services.telemetry.clearEvents(); await DoHController.init(); } Services.prefs.setStringPref("network.trr.confirmationNS", "skip"); let preferencesOpen = new Promise(res => open_preferences(res)); registerCleanupFunction(async () => { await resetPrefs(); gBrowser.removeCurrentTab(); Services.prefs.clearUserPref("network.trr.confirmationNS"); }); async function openConnectionsSubDialog() { /* The connection dialog has type="child", So it has to be opened as a sub dialog of the main pref tab. Prefs only get updated after the subdialog is confirmed & closed */ let dialog = await openAndLoadSubDialog(SUBDIALOG_URL); ok(dialog, "connection window opened"); return dialog; } function waitForPrefObserver(name) { return new Promise(resolve => { const observer = { observe(aSubject, aTopic, aData) { if (aData == name) { Services.prefs.removeObserver(name, observer); resolve(); } }, }; Services.prefs.addObserver(name, observer); }); } async function testWithProperties(props, startTime) { info( Date.now() - startTime + ": testWithProperties: testing with " + JSON.stringify(props) ); // There are two different signals that the DoHController is ready, depending // on the config being tested. If we're setting the TRR mode pref, we should // expect the disable-heuristics pref to be set as the signal. Else, we can // expect the self-enabled pref as the signal. let rolloutReadyPromise; if (props.hasOwnProperty(TRR_MODE_PREF)) { if ( [2, 3, 5].includes(props[TRR_MODE_PREF]) && props.hasOwnProperty(ROLLOUT_ENABLED_PREF) ) { // Only initialize the promise if we're going to enable the rollout - // otherwise we will never await it, which could cause a leak if it doesn't // end up resolving. rolloutReadyPromise = waitForPrefObserver(HEURISTICS_DISABLED_PREF); } Services.prefs.setIntPref(TRR_MODE_PREF, props[TRR_MODE_PREF]); } if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) { if (!rolloutReadyPromise) { rolloutReadyPromise = waitForPrefObserver(ROLLOUT_SELF_ENABLED_PREF); } Services.prefs.setBoolPref( ROLLOUT_ENABLED_PREF, props[ROLLOUT_ENABLED_PREF] ); await rolloutReadyPromise; } if (props.hasOwnProperty(TRR_CUSTOM_URI_PREF)) { Services.prefs.setStringPref( TRR_CUSTOM_URI_PREF, props[TRR_CUSTOM_URI_PREF] ); } if (props.hasOwnProperty(TRR_URI_PREF)) { Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]); } let dialog = await openConnectionsSubDialog(); await dialog.uiReady; info( Date.now() - startTime + ": testWithProperties: connections dialog now open" ); let doc = dialog.document; let win = doc.ownerGlobal; let dialogElement = doc.getElementById("ConnectionsDialog"); let dialogClosingPromise = BrowserTestUtils.waitForEvent( dialogElement, "dialogclosing" ); let modeCheckbox = doc.querySelector(modeCheckboxSelector); let uriTextbox = doc.querySelector(uriTextboxSelector); let resolverMenulist = doc.querySelector(resolverMenulistSelector); let uriPrefChangedPromise; let modePrefChangedPromise; let disableHeuristicsPrefChangedPromise; if (props.hasOwnProperty("expectedModeChecked")) { await TestUtils.waitForCondition( () => modeCheckbox.checked === props.expectedModeChecked ); is( modeCheckbox.checked, props.expectedModeChecked, "mode checkbox has expected checked state" ); } if (props.hasOwnProperty("expectedUriValue")) { await TestUtils.waitForCondition( () => uriTextbox.value === props.expectedUriValue ); is( uriTextbox.value, props.expectedUriValue, "URI textbox has expected value" ); } if (props.hasOwnProperty("expectedResolverListValue")) { await TestUtils.waitForCondition( () => resolverMenulist.value === props.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); if (props.hasOwnProperty("expectedDisabledHeuristics")) { disableHeuristicsPrefChangedPromise = waitForPrefObserver( HEURISTICS_DISABLED_PREF ); } info( Date.now() - startTime + ": testWithProperties: clickMode, pref changed" ); modeCheckbox.scrollIntoView(); 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); info( Date.now() - startTime + ": testWithProperties: inputUriKeys, pref changed, now enter the new value" ); uriTextbox.focus(); uriTextbox.value = props.inputUriKeys; uriTextbox.dispatchEvent(new win.Event("input", { bubbles: true })); uriTextbox.dispatchEvent(new win.Event("change", { bubbles: true })); info( Date.now() - startTime + ": testWithProperties: inputUriKeys, input and change events dispatched" ); } info(Date.now() - startTime + ": testWithProperties: calling acceptDialog"); dialogElement.acceptDialog(); info( Date.now() - startTime + ": testWithProperties: waiting for the dialogClosingPromise" ); let dialogClosingEvent = await dialogClosingPromise; ok(dialogClosingEvent, "connection window closed"); info( Date.now() - startTime + ": testWithProperties: waiting for any of uri and mode prefs to change" ); await Promise.all([ uriPrefChangedPromise, modePrefChangedPromise, disableHeuristicsPrefChangedPromise, ]); info(Date.now() - startTime + ": testWithProperties: prefs changed"); if (props.hasOwnProperty("expectedFinalUriPref")) { if (props.expectedFinalUriPref) { let uriPref = Services.prefs.getStringPref(TRR_URI_PREF); is( uriPref, props.expectedFinalUriPref, "uri pref ended up with the expected value" ); } else { ok( !Services.prefs.prefHasUserValue(TRR_URI_PREF), "uri pref ended up with the expected value (unset)" ); } } if (props.hasOwnProperty("expectedModePref")) { let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); is( modePref, props.expectedModePref, "mode pref ended up with the expected value" ); } if (props.hasOwnProperty("expectedDisabledHeuristics")) { let disabledHeuristicsPref = Services.prefs.getBoolPref( HEURISTICS_DISABLED_PREF ); is( disabledHeuristicsPref, props.expectedDisabledHeuristics, "disable-heuristics 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"); } add_task(async function default_values() { let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); let uriPrefHasUserValue = Services.prefs.prefHasUserValue(TRR_URI_PREF); let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); is( modePref, defaultPrefValues[TRR_MODE_PREF], `Actual value of ${TRR_MODE_PREF} matches expected default value` ); ok( !uriPrefHasUserValue, `Actual value of ${TRR_URI_PREF} matches expected default value (unset)` ); is( customUriPref, defaultPrefValues[TRR_CUSTOM_URI_PREF], `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value` ); }); let testVariations = [ // verify state with defaults { name: "default", expectedModePref: 5, expectedUriValue: "" }, // verify each of the modes maps to the correct checked state { name: "mode 0", [TRR_MODE_PREF]: 0, expectedModeChecked: false }, { name: "mode 1", [TRR_MODE_PREF]: 1, expectedModeChecked: false, }, { 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: false, }, { name: "mode 5", [TRR_MODE_PREF]: 5, expectedModeChecked: false }, // verify an out of bounds mode value maps to the correct checked state { name: "mode out-of-bounds", [TRR_MODE_PREF]: 77, expectedModeChecked: false, }, // verify automatic heuristics states { name: "heuristics on and mode unset", [TRR_MODE_PREF]: 0, [ROLLOUT_ENABLED_PREF]: true, expectedModeChecked: true, }, { name: "heuristics on and mode set to 2", [TRR_MODE_PREF]: 2, [ROLLOUT_ENABLED_PREF]: true, expectedModeChecked: true, }, { name: "heuristics on but disabled, mode unset", [TRR_MODE_PREF]: 5, [ROLLOUT_ENABLED_PREF]: true, expectedModeChecked: false, }, { name: "heuristics on but disabled, mode set to 2", [TRR_MODE_PREF]: 2, [ROLLOUT_ENABLED_PREF]: true, expectedModeChecked: true, }, // verify toggling the checkbox gives the right outcomes { name: "toggle mode on", clickMode: true, expectedModeValue: 2, expectedUriValue: "", expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, }, { name: "toggle mode off", [TRR_MODE_PREF]: 2, expectedModeChecked: true, clickMode: true, expectedModePref: 5, }, { name: "toggle mode off when on due to heuristics", [TRR_MODE_PREF]: 0, [ROLLOUT_ENABLED_PREF]: true, expectedModeChecked: true, clickMode: true, expectedModePref: 5, expectedDisabledHeuristics: true, }, // Test selecting non-default, non-custom TRR provider, NextDNS. { name: "Select NextDNS as TRR provider", [TRR_MODE_PREF]: 2, selectResolver: SECOND_RESOLVER_VALUE, expectedFinalUriPref: SECOND_RESOLVER_VALUE, }, // Test selecting non-default, non-custom TRR provider, NextDNS, // with DoH not enabled. The provider selection should stick. { name: "Select NextDNS as TRR provider in mode 0", [TRR_MODE_PREF]: 0, selectResolver: SECOND_RESOLVER_VALUE, expectedFinalUriPref: SECOND_RESOLVER_VALUE, }, { name: "return to default from NextDNS", [TRR_MODE_PREF]: 2, [TRR_URI_PREF]: SECOND_RESOLVER_VALUE, expectedResolverListValue: SECOND_RESOLVER_VALUE, selectResolver: DEFAULT_RESOLVER_VALUE, expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, }, // 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, selectResolver: "custom", expectedUriValue: "https://example.com", expectedFinalUriPref: "https://example.com", expectedFinalCustomUriPref: "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", }, { 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", 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_MODE_PREF]: 2, [TRR_URI_PREF]: "https://example.com", [TRR_CUSTOM_URI_PREF]: "", expectedUriValue: "https://example.com", expectedResolverListValue: "custom", expectedFinalUriPref: "https://example.com", expectedFinalCustomUriPref: "https://example.com", }, ]; for (let props of testVariations) { add_task(async function testVariation() { await preferencesOpen; let startTime = Date.now(); info("starting test: " + props.name); await testWithProperties(props, startTime); await resetPrefs(); }); } add_task(async function testRemoteSettingsEnable() { // Enable the rollout. await DoHTestUtils.loadRemoteSettingsConfig({ providers: "example-1, example-2", rolloutEnabled: true, steeringEnabled: false, steeringProviders: "", autoDefaultEnabled: false, autoDefaultProviders: "", id: "global", }); let doTest = async (cancelOrAccept = "cancel") => { let dialog = await openConnectionsSubDialog(); await dialog.uiReady; let doc = dialog.document; let dialogElement = doc.getElementById("ConnectionsDialog"); let modeCheckbox = doc.querySelector(modeCheckboxSelector); ok(modeCheckbox.checked, "The mode checkbox should be checked."); let dialogClosingPromise = BrowserTestUtils.waitForEvent( dialogElement, "dialogclosing" ); if (cancelOrAccept == "cancel") { dialogElement.cancelDialog(); } else { dialogElement.acceptDialog(); } await dialogClosingPromise; if (cancelOrAccept == "cancel") { try { await TestUtils.waitForCondition(() => Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics") ); ok(false, "Heuristics were disabled when they shouldn't have been!"); } catch (e) { ok(true, "Heuristics remained enabled."); } is(Services.prefs.getStringPref("network.trr.uri"), ""); ok(!Services.prefs.prefHasUserValue("network.trr.mode")); } else { // If accepting, the chosen provider is persisted to network.trr.uri // and heuristics should get disabled. await TestUtils.waitForCondition(() => Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics") ); ok( Services.prefs.getBoolPref("doh-rollout.disable-heuristics"), "Heurstics were disabled." ); is( Services.prefs.getStringPref("network.trr.uri"), DEFAULT_RESOLVER_VALUE ); is(Services.prefs.getIntPref("network.trr.mode"), 2); } }; for (let action of ["cancel", "accept"]) { await doTest(action); } }); add_task(async function testEnterprisePolicy() { async function closeDialog(dialog) { let dialogClosingPromise = BrowserTestUtils.waitForEvent( dialog, "dialogclosing" ); dialog.cancelDialog(); await dialogClosingPromise; } async function withPolicy(policy, fn, preFn = () => {}) { await resetPrefs(); PoliciesPrefTracker.start(); await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy); await preFn(); let dialog = await openConnectionsSubDialog(); await dialog.uiReady; let doc = dialog.document; let dialogElement = doc.getElementById("ConnectionsDialog"); let modeCheckbox = doc.querySelector(modeCheckboxSelector); let resolverMenulist = doc.querySelector(resolverMenulistSelector); let uriTextbox = doc.querySelector(uriTextboxSelector); await fn({ dialog, dialogElement, modeCheckbox, resolverMenulist, doc, uriTextbox, }); await closeDialog(dialogElement); EnterprisePolicyTesting.resetRunOnceState(); PoliciesPrefTracker.stop(); } info("Check that a locked policy does not allow any changes in the UI"); await withPolicy( { policies: { DNSOverHTTPS: { Enabled: true, ProviderURL: "https://examplelocked.com/provider", ExcludedDomains: ["examplelocked.com", "example.org"], Locked: true, }, }, }, async res => { ok(res.modeCheckbox.checked, "The mode checkbox should be checked."); is(res.modeCheckbox.disabled, true, "The checkbox should be locked."); is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); is( res.resolverMenulist.disabled, true, "The resolver list should be locked." ); is(res.uriTextbox.disabled, true, "The custom URI should be locked."); } ); info("Check that an unlocked policy has editable fields in the dialog"); await withPolicy( { policies: { DNSOverHTTPS: { Enabled: true, ProviderURL: "https://example.com/provider", ExcludedDomains: ["example.com", "example.org"], }, }, }, async res => { ok(res.modeCheckbox.checked, "The mode checkbox should be checked."); is( res.modeCheckbox.disabled, false, "The checkbox should not be locked." ); is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); is( res.resolverMenulist.disabled, false, "The resolver list should not be locked." ); is( res.uriTextbox.value, "https://example.com/provider", "Expected custom resolver" ); is( res.uriTextbox.disabled, false, "The custom URI should not be locked." ); } ); info("Check that a locked disabled policy disables the buttons"); await withPolicy( { policies: { DNSOverHTTPS: { Enabled: false, ProviderURL: "https://example.com/provider", ExcludedDomains: ["example.com", "example.org"], Locked: true, }, }, }, async res => { ok(!res.modeCheckbox.checked, "The mode checkbox should be unchecked."); is(res.modeCheckbox.disabled, true, "The checkbox should be locked."); is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); is( res.resolverMenulist.disabled, true, "The resolver list should be locked." ); is(res.uriTextbox.disabled, true, "The custom URI should be locked."); } ); info("Check that an unlocked disabled policy has editable fields"); await withPolicy( { policies: { DNSOverHTTPS: { Enabled: false, ProviderURL: "https://example.com/provider", ExcludedDomains: ["example.com", "example.org"], }, }, }, async res => { ok(!res.modeCheckbox.checked, "The mode checkbox should be unchecked."); is( res.modeCheckbox.disabled, false, "The checkbox should not be locked." ); is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); is( res.resolverMenulist.disabled, true, "The resolver list should be locked." ); is(res.uriTextbox.disabled, true, "The custom URI should be locked."); } ); info("Check that the remote settings config doesn't override the policy"); await withPolicy( { policies: { DNSOverHTTPS: { Enabled: true, ProviderURL: "https://example.com/provider", ExcludedDomains: ["example.com", "example.org"], }, }, }, async res => { ok(res.modeCheckbox.checked, "The mode checkbox should be checked."); is( res.modeCheckbox.disabled, false, "The checkbox should not be locked." ); is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); is( res.resolverMenulist.disabled, false, "The resolver list should not be locked." ); is( res.uriTextbox.value, "https://example.com/provider", "Expected custom resolver" ); is( res.uriTextbox.disabled, false, "The custom URI should not be locked." ); }, async function runAfterSettingPolicy() { await DoHTestUtils.loadRemoteSettingsConfig({ providers: "example-1, example-2", rolloutEnabled: true, steeringEnabled: false, steeringProviders: "", autoDefaultEnabled: false, autoDefaultProviders: "", id: "global", }); } ); });