forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			617 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			617 lines
		
	
	
	
		
			19 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/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
/*
 | 
						|
 * This module runs the automated heuristics to enable/disable DoH on different
 | 
						|
 * networks. Heuristics are run at startup and upon network changes.
 | 
						|
 * Heuristics are disabled if the user sets their DoH provider or mode manually.
 | 
						|
 */
 | 
						|
var EXPORTED_SYMBOLS = ["DoHController"];
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { XPCOMUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(this, {
 | 
						|
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
 | 
						|
  ClientID: "resource://gre/modules/ClientID.jsm",
 | 
						|
  ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
 | 
						|
  Config: "resource:///modules/DoHConfig.jsm",
 | 
						|
  Heuristics: "resource:///modules/DoHHeuristics.jsm",
 | 
						|
  Preferences: "resource://gre/modules/Preferences.jsm",
 | 
						|
  setTimeout: "resource://gre/modules/Timer.jsm",
 | 
						|
  clearTimeout: "resource://gre/modules/Timer.jsm",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  this,
 | 
						|
  "kDebounceTimeout",
 | 
						|
  "doh-rollout.network-debounce-timeout",
 | 
						|
  1000
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "gCaptivePortalService",
 | 
						|
  "@mozilla.org/network/captive-portal-service;1",
 | 
						|
  "nsICaptivePortalService"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "gDNSService",
 | 
						|
  "@mozilla.org/network/dns-service;1",
 | 
						|
  "nsIDNSService"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "gNetworkLinkService",
 | 
						|
  "@mozilla.org/network/network-link-service;1",
 | 
						|
  "nsINetworkLinkService"
 | 
						|
);
 | 
						|
 | 
						|
// Stores whether we've done first-run.
 | 
						|
const FIRST_RUN_PREF = "doh-rollout.doneFirstRun";
 | 
						|
 | 
						|
// Records if the user opted in/out of DoH study by clicking on doorhanger
 | 
						|
const DOORHANGER_USER_DECISION_PREF = "doh-rollout.doorhanger-decision";
 | 
						|
 | 
						|
// Set when we detect that the user set their DoH provider or mode manually.
 | 
						|
// If set, we don't run heuristics.
 | 
						|
const DISABLED_PREF = "doh-rollout.disable-heuristics";
 | 
						|
 | 
						|
// Set when we detect either a non-DoH enterprise policy, or a DoH policy that
 | 
						|
// tells us to disable it. This pref's effect is to suppress the opt-out CFR.
 | 
						|
const SKIP_HEURISTICS_PREF = "doh-rollout.skipHeuristicsCheck";
 | 
						|
 | 
						|
// Whether to clear doh-rollout.mode on shutdown. When false, the mode value
 | 
						|
// that exists at shutdown will be used at startup until heuristics re-run.
 | 
						|
const CLEAR_ON_SHUTDOWN_PREF = "doh-rollout.clearModeOnShutdown";
 | 
						|
 | 
						|
const BREADCRUMB_PREF = "doh-rollout.self-enabled";
 | 
						|
 | 
						|
// Necko TRR prefs to watch for user-set values.
 | 
						|
const NETWORK_TRR_MODE_PREF = "network.trr.mode";
 | 
						|
const NETWORK_TRR_URI_PREF = "network.trr.uri";
 | 
						|
 | 
						|
const TRR_LIST_PREF = "network.trr.resolvers";
 | 
						|
 | 
						|
const ROLLOUT_MODE_PREF = "doh-rollout.mode";
 | 
						|
const ROLLOUT_URI_PREF = "doh-rollout.uri";
 | 
						|
 | 
						|
const TRR_SELECT_DRY_RUN_RESULT_PREF =
 | 
						|
  "doh-rollout.trr-selection.dry-run-result";
 | 
						|
 | 
						|
const HEURISTICS_TELEMETRY_CATEGORY = "doh";
 | 
						|
const TRRSELECT_TELEMETRY_CATEGORY = "security.doh.trrPerformance";
 | 
						|
 | 
						|
const kLinkStatusChangedTopic = "network:link-status-changed";
 | 
						|
const kConnectivityTopic = "network:captive-portal-connectivity";
 | 
						|
const kPrefChangedTopic = "nsPref:changed";
 | 
						|
 | 
						|
// Helper function to hash the network ID concatenated with telemetry client ID.
 | 
						|
// This prevents us from being able to tell if 2 clients are on the same network.
 | 
						|
function getHashedNetworkID() {
 | 
						|
  let currentNetworkID = gNetworkLinkService.networkID;
 | 
						|
  if (!currentNetworkID) {
 | 
						|
    return "";
 | 
						|
  }
 | 
						|
 | 
						|
  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
 | 
						|
    Ci.nsICryptoHash
 | 
						|
  );
 | 
						|
 | 
						|
  hasher.init(Ci.nsICryptoHash.SHA256);
 | 
						|
  // Concat the client ID with the network ID before hashing.
 | 
						|
  let clientNetworkID = ClientID.getClientID() + currentNetworkID;
 | 
						|
  hasher.update(
 | 
						|
    clientNetworkID.split("").map(c => c.charCodeAt(0)),
 | 
						|
    clientNetworkID.length
 | 
						|
  );
 | 
						|
  return hasher.finish(true);
 | 
						|
}
 | 
						|
 | 
						|
const DoHController = {
 | 
						|
  _heuristicsAreEnabled: false,
 | 
						|
 | 
						|
  async init() {
 | 
						|
    await this.migrateLocalStoragePrefs();
 | 
						|
    await this.migrateOldTrrMode();
 | 
						|
    await this.migrateNextDNSEndpoint();
 | 
						|
 | 
						|
    Services.telemetry.setEventRecordingEnabled(
 | 
						|
      HEURISTICS_TELEMETRY_CATEGORY,
 | 
						|
      true
 | 
						|
    );
 | 
						|
    Services.telemetry.setEventRecordingEnabled(
 | 
						|
      TRRSELECT_TELEMETRY_CATEGORY,
 | 
						|
      true
 | 
						|
    );
 | 
						|
 | 
						|
    Services.obs.addObserver(this, Config.kConfigUpdateTopic);
 | 
						|
    Preferences.observe(NETWORK_TRR_MODE_PREF, this);
 | 
						|
    Preferences.observe(NETWORK_TRR_URI_PREF, this);
 | 
						|
 | 
						|
    if (Config.enabled) {
 | 
						|
      await this.maybeEnableHeuristics();
 | 
						|
    } else if (Preferences.get(FIRST_RUN_PREF, false)) {
 | 
						|
      await this.rollback();
 | 
						|
    }
 | 
						|
 | 
						|
    this._asyncShutdownBlocker = async () => {
 | 
						|
      await this.disableHeuristics("shutdown");
 | 
						|
    };
 | 
						|
 | 
						|
    AsyncShutdown.profileBeforeChange.addBlocker(
 | 
						|
      "DoHController: clear state and remove observers",
 | 
						|
      this._asyncShutdownBlocker
 | 
						|
    );
 | 
						|
 | 
						|
    Preferences.set(FIRST_RUN_PREF, true);
 | 
						|
  },
 | 
						|
 | 
						|
  // Also used by tests to reset DoHController state (prefs are not cleared
 | 
						|
  // here - tests do that when needed between _uninit and init).
 | 
						|
  async _uninit() {
 | 
						|
    Services.obs.removeObserver(this, Config.kConfigUpdateTopic);
 | 
						|
    Preferences.ignore(NETWORK_TRR_MODE_PREF, this);
 | 
						|
    Preferences.ignore(NETWORK_TRR_URI_PREF, this);
 | 
						|
    AsyncShutdown.profileBeforeChange.removeBlocker(this._asyncShutdownBlocker);
 | 
						|
    await this.disableHeuristics("shutdown");
 | 
						|
  },
 | 
						|
 | 
						|
  // Called to reset state when a new config is available.
 | 
						|
  async reset() {
 | 
						|
    await this._uninit();
 | 
						|
    await this.init();
 | 
						|
  },
 | 
						|
 | 
						|
  async migrateLocalStoragePrefs() {
 | 
						|
    const BALROG_MIGRATION_COMPLETED_PREF = "doh-rollout.balrog-migration-done";
 | 
						|
    const ADDON_ID = "doh-rollout@mozilla.org";
 | 
						|
 | 
						|
    // Migrate updated local storage item names. If this has already been done once, skip the migration
 | 
						|
    const isMigrated = Preferences.get(BALROG_MIGRATION_COMPLETED_PREF, false);
 | 
						|
 | 
						|
    if (isMigrated) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let policy = WebExtensionPolicy.getByID(ADDON_ID);
 | 
						|
    if (!policy) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const storagePrincipal = ExtensionStorageIDB.getStoragePrincipal(
 | 
						|
      policy.extension
 | 
						|
    );
 | 
						|
    const idbConn = await ExtensionStorageIDB.open(storagePrincipal);
 | 
						|
 | 
						|
    // Previously, the DoH heuristics were bundled as an add-on. Early versions
 | 
						|
    // of this add-on used local storage instead of prefs to persist state. This
 | 
						|
    // function migrates the values that are still relevant to their new pref
 | 
						|
    // counterparts.
 | 
						|
    const legacyLocalStorageKeys = [
 | 
						|
      "doneFirstRun",
 | 
						|
      DOORHANGER_USER_DECISION_PREF,
 | 
						|
      DISABLED_PREF,
 | 
						|
    ];
 | 
						|
 | 
						|
    for (let item of legacyLocalStorageKeys) {
 | 
						|
      let data = await idbConn.get(item);
 | 
						|
      let value = data[item];
 | 
						|
 | 
						|
      if (data.hasOwnProperty(item)) {
 | 
						|
        let migratedName = item;
 | 
						|
 | 
						|
        if (!item.startsWith("doh-rollout.")) {
 | 
						|
          migratedName = "doh-rollout." + item;
 | 
						|
        }
 | 
						|
 | 
						|
        Preferences.set(migratedName, value);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    await idbConn.clear();
 | 
						|
    await idbConn.close();
 | 
						|
 | 
						|
    // Set pref to skip this function in the future.
 | 
						|
    Preferences.set(BALROG_MIGRATION_COMPLETED_PREF, true);
 | 
						|
  },
 | 
						|
 | 
						|
  // Previous versions of the DoH frontend worked by setting network.trr.mode
 | 
						|
  // directly to turn DoH on/off. This makes sure we clear that value and also
 | 
						|
  // the pref we formerly used to track changes to it.
 | 
						|
  async migrateOldTrrMode() {
 | 
						|
    const PREVIOUS_TRR_MODE_PREF = "doh-rollout.previous.trr.mode";
 | 
						|
 | 
						|
    if (Preferences.get(PREVIOUS_TRR_MODE_PREF) === undefined) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (Preferences.get(NETWORK_TRR_MODE_PREF) !== 5) {
 | 
						|
      Preferences.reset(NETWORK_TRR_MODE_PREF);
 | 
						|
    }
 | 
						|
    Preferences.reset(PREVIOUS_TRR_MODE_PREF);
 | 
						|
  },
 | 
						|
 | 
						|
  async migrateNextDNSEndpoint() {
 | 
						|
    // NextDNS endpoint changed from trr.dns.nextdns.io to firefox.dns.nextdns.io
 | 
						|
    // This migration updates any pref values that might be using the old value
 | 
						|
    // to the new one. We support values that match the exact URL that shipped
 | 
						|
    // in the network.trr.resolvers pref in prior versions of the browser.
 | 
						|
    // The migration is a direct static replacement of the string.
 | 
						|
    const oldURL = "https://trr.dns.nextdns.io/";
 | 
						|
    const newURL = "https://firefox.dns.nextdns.io/";
 | 
						|
    const prefsToMigrate = [
 | 
						|
      "network.trr.resolvers",
 | 
						|
      "network.trr.uri",
 | 
						|
      "network.trr.custom_uri",
 | 
						|
      "doh-rollout.trr-selection.dry-run-result",
 | 
						|
      "doh-rollout.uri",
 | 
						|
    ];
 | 
						|
 | 
						|
    for (let pref of prefsToMigrate) {
 | 
						|
      if (!Preferences.isSet(pref)) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      Preferences.set(pref, Preferences.get(pref).replaceAll(oldURL, newURL));
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // The "maybe" is because there are two cases when we don't enable heuristics:
 | 
						|
  // 1. If we detect that TRR mode or URI have user values, or we previously
 | 
						|
  //    detected this (i.e. DISABLED_PREF is true)
 | 
						|
  // 2. If there are any non-DoH enterprise policies active
 | 
						|
  async maybeEnableHeuristics() {
 | 
						|
    if (Preferences.get(DISABLED_PREF)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let policyResult = await Heuristics.checkEnterprisePolicy();
 | 
						|
 | 
						|
    if (["policy_without_doh", "disable_doh"].includes(policyResult)) {
 | 
						|
      await this.setState("policyDisabled");
 | 
						|
      Preferences.set(SKIP_HEURISTICS_PREF, true);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    Preferences.reset(SKIP_HEURISTICS_PREF);
 | 
						|
 | 
						|
    if (
 | 
						|
      Preferences.isSet(NETWORK_TRR_MODE_PREF) ||
 | 
						|
      Preferences.isSet(NETWORK_TRR_URI_PREF)
 | 
						|
    ) {
 | 
						|
      await this.setState("manuallyDisabled");
 | 
						|
      Preferences.set(DISABLED_PREF, true);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.runTRRSelection();
 | 
						|
    await this.runHeuristics("startup");
 | 
						|
    Services.obs.addObserver(this, kLinkStatusChangedTopic);
 | 
						|
    Services.obs.addObserver(this, kConnectivityTopic);
 | 
						|
 | 
						|
    this._heuristicsAreEnabled = true;
 | 
						|
  },
 | 
						|
 | 
						|
  _lastHeuristicsRunTimestamp: 0,
 | 
						|
  async runHeuristics(evaluateReason) {
 | 
						|
    let start = Date.now();
 | 
						|
    // If this function is called in quick succession, _lastHeuristicsRunTimestamp
 | 
						|
    // might be refreshed while we are still awaiting Heuristics.run() below.
 | 
						|
    this._lastHeuristicsRunTimestamp = start;
 | 
						|
 | 
						|
    let results = await Heuristics.run();
 | 
						|
 | 
						|
    if (
 | 
						|
      !gNetworkLinkService.isLinkUp ||
 | 
						|
      this._lastDebounceTimestamp > start ||
 | 
						|
      this._lastHeuristicsRunTimestamp > start ||
 | 
						|
      gCaptivePortalService.state == gCaptivePortalService.LOCKED_PORTAL
 | 
						|
    ) {
 | 
						|
      // If the network is currently down or there was a debounce triggered
 | 
						|
      // while we were running heuristics, it means the network fluctuated
 | 
						|
      // during this heuristics run. We simply discard the results in this case.
 | 
						|
      // Same thing if there was another heuristics run triggered or if we have
 | 
						|
      // detected a locked captive portal while this one was ongoing.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let decision = Object.values(results).includes(Heuristics.DISABLE_DOH)
 | 
						|
      ? Heuristics.DISABLE_DOH
 | 
						|
      : Heuristics.ENABLE_DOH;
 | 
						|
 | 
						|
    let getCaptiveStateString = () => {
 | 
						|
      switch (gCaptivePortalService.state) {
 | 
						|
        case gCaptivePortalService.NOT_CAPTIVE:
 | 
						|
          return "not_captive";
 | 
						|
        case gCaptivePortalService.UNLOCKED_PORTAL:
 | 
						|
          return "unlocked";
 | 
						|
        case gCaptivePortalService.LOCKED_PORTAL:
 | 
						|
          return "locked";
 | 
						|
        default:
 | 
						|
          return "unknown";
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    let resultsForTelemetry = {
 | 
						|
      evaluateReason,
 | 
						|
      steeredProvider: "",
 | 
						|
      captiveState: getCaptiveStateString(),
 | 
						|
      // NOTE: This might not yet be available after a network change. We mainly
 | 
						|
      // care about the startup case though - we want to look at whether the
 | 
						|
      // heuristics result is consistent for networkIDs often seen at startup.
 | 
						|
      // TODO: Use this data to implement cached results to use early at startup.
 | 
						|
      networkID: getHashedNetworkID(),
 | 
						|
    };
 | 
						|
 | 
						|
    if (results.steeredProvider) {
 | 
						|
      gDNSService.setDetectedTrrURI(results.steeredProvider.uri);
 | 
						|
      resultsForTelemetry.steeredProvider = results.steeredProvider.name;
 | 
						|
    }
 | 
						|
 | 
						|
    if (decision === Heuristics.DISABLE_DOH) {
 | 
						|
      await this.setState("disabled");
 | 
						|
    } else {
 | 
						|
      await this.setState("enabled");
 | 
						|
    }
 | 
						|
 | 
						|
    // For telemetry, we group the heuristics results into three categories.
 | 
						|
    // Only heuristics with a DISABLE_DOH result are included.
 | 
						|
    // Each category is finally included in the event as a comma-separated list.
 | 
						|
    let canaries = [];
 | 
						|
    let filtering = [];
 | 
						|
    let enterprise = [];
 | 
						|
    let platform = [];
 | 
						|
 | 
						|
    for (let [heuristicName, result] of Object.entries(results)) {
 | 
						|
      if (result !== Heuristics.DISABLE_DOH) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (["canary", "zscalerCanary"].includes(heuristicName)) {
 | 
						|
        canaries.push(heuristicName);
 | 
						|
      } else if (
 | 
						|
        ["browserParent", "google", "youtube"].includes(heuristicName)
 | 
						|
      ) {
 | 
						|
        filtering.push(heuristicName);
 | 
						|
      } else if (
 | 
						|
        ["policy", "modifiedRoots", "thirdPartyRoots"].includes(heuristicName)
 | 
						|
      ) {
 | 
						|
        enterprise.push(heuristicName);
 | 
						|
      } else if (["vpn", "proxy", "nrpt"].includes(heuristicName)) {
 | 
						|
        platform.push(heuristicName);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    resultsForTelemetry.canaries = canaries.join(",");
 | 
						|
    resultsForTelemetry.filtering = filtering.join(",");
 | 
						|
    resultsForTelemetry.enterprise = enterprise.join(",");
 | 
						|
    resultsForTelemetry.platform = platform.join(",");
 | 
						|
 | 
						|
    Services.telemetry.recordEvent(
 | 
						|
      HEURISTICS_TELEMETRY_CATEGORY,
 | 
						|
      "evaluate_v2",
 | 
						|
      "heuristics",
 | 
						|
      decision,
 | 
						|
      resultsForTelemetry
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async setState(state) {
 | 
						|
    switch (state) {
 | 
						|
      case "disabled":
 | 
						|
        Preferences.set(ROLLOUT_MODE_PREF, 0);
 | 
						|
        break;
 | 
						|
      case "UIOk":
 | 
						|
        Preferences.set(BREADCRUMB_PREF, true);
 | 
						|
        break;
 | 
						|
      case "enabled":
 | 
						|
        Preferences.set(ROLLOUT_MODE_PREF, 2);
 | 
						|
        Preferences.set(BREADCRUMB_PREF, true);
 | 
						|
        break;
 | 
						|
      case "policyDisabled":
 | 
						|
      case "manuallyDisabled":
 | 
						|
      case "UIDisabled":
 | 
						|
        Preferences.reset(BREADCRUMB_PREF);
 | 
						|
      // Fall through.
 | 
						|
      case "rollback":
 | 
						|
        Preferences.reset(ROLLOUT_MODE_PREF);
 | 
						|
        break;
 | 
						|
      case "shutdown":
 | 
						|
        if (Preferences.get(CLEAR_ON_SHUTDOWN_PREF, true)) {
 | 
						|
          Preferences.reset(ROLLOUT_MODE_PREF);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    Services.telemetry.recordEvent(
 | 
						|
      HEURISTICS_TELEMETRY_CATEGORY,
 | 
						|
      "state",
 | 
						|
      state,
 | 
						|
      "null"
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async disableHeuristics(state) {
 | 
						|
    await this.setState(state);
 | 
						|
 | 
						|
    if (!this._heuristicsAreEnabled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    Services.obs.removeObserver(this, kLinkStatusChangedTopic);
 | 
						|
    Services.obs.removeObserver(this, kConnectivityTopic);
 | 
						|
    this._heuristicsAreEnabled = false;
 | 
						|
  },
 | 
						|
 | 
						|
  async rollback() {
 | 
						|
    await this.disableHeuristics("rollback");
 | 
						|
  },
 | 
						|
 | 
						|
  async runTRRSelection() {
 | 
						|
    // If persisting the selection is disabled, clear the existing
 | 
						|
    // selection.
 | 
						|
    if (!Config.trrSelection.commitResult) {
 | 
						|
      Preferences.reset(ROLLOUT_URI_PREF);
 | 
						|
    }
 | 
						|
 | 
						|
    if (!Config.trrSelection.enabled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (Preferences.isSet(ROLLOUT_URI_PREF)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.runTRRSelectionDryRun();
 | 
						|
 | 
						|
    // If persisting the selection is disabled, don't commit the value.
 | 
						|
    if (!Config.trrSelection.commitResult) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    Preferences.set(
 | 
						|
      ROLLOUT_URI_PREF,
 | 
						|
      Preferences.get(TRR_SELECT_DRY_RUN_RESULT_PREF)
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async runTRRSelectionDryRun() {
 | 
						|
    if (Preferences.isSet(TRR_SELECT_DRY_RUN_RESULT_PREF)) {
 | 
						|
      // Check whether the existing dry-run-result is in the default
 | 
						|
      // list of TRRs. If it is, all good. Else, run the dry run again.
 | 
						|
      let dryRunResult = Preferences.get(TRR_SELECT_DRY_RUN_RESULT_PREF);
 | 
						|
      let defaultTRRs = JSON.parse(
 | 
						|
        Services.prefs.getDefaultBranch("").getCharPref(TRR_LIST_PREF)
 | 
						|
      );
 | 
						|
      let dryRunResultIsValid = defaultTRRs.some(
 | 
						|
        trr => trr.url == dryRunResult
 | 
						|
      );
 | 
						|
      if (dryRunResultIsValid) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let setDryRunResultAndRecordTelemetry = trr => {
 | 
						|
      Preferences.set(TRR_SELECT_DRY_RUN_RESULT_PREF, trr);
 | 
						|
      Services.telemetry.recordEvent(
 | 
						|
        TRRSELECT_TELEMETRY_CATEGORY,
 | 
						|
        "trrselect",
 | 
						|
        "dryrunresult",
 | 
						|
        trr.substring(0, 40) // Telemetry payload max length
 | 
						|
      );
 | 
						|
    };
 | 
						|
 | 
						|
    if (Cu.isInAutomation) {
 | 
						|
      // For mochitests, just record telemetry with a dummy result.
 | 
						|
      // TRRPerformance.jsm is tested in xpcshell.
 | 
						|
      setDryRunResultAndRecordTelemetry("https://dummytrr.com/query");
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Importing the module here saves us from having to do it at startup, and
 | 
						|
    // ensures tests have time to set prefs before the module initializes.
 | 
						|
    let { TRRRacer } = ChromeUtils.import(
 | 
						|
      "resource:///modules/TRRPerformance.jsm"
 | 
						|
    );
 | 
						|
    await new Promise(resolve => {
 | 
						|
      let racer = new TRRRacer(() => {
 | 
						|
        setDryRunResultAndRecordTelemetry(racer.getFastestTRR(true));
 | 
						|
        resolve();
 | 
						|
      });
 | 
						|
      racer.run();
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  observe(subject, topic, data) {
 | 
						|
    switch (topic) {
 | 
						|
      case kLinkStatusChangedTopic:
 | 
						|
        this.onConnectionChanged();
 | 
						|
        break;
 | 
						|
      case kConnectivityTopic:
 | 
						|
        this.onConnectivityAvailable();
 | 
						|
        break;
 | 
						|
      case kPrefChangedTopic:
 | 
						|
        this.onPrefChanged(data);
 | 
						|
        break;
 | 
						|
      case Config.kConfigUpdateTopic:
 | 
						|
        this.reset();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async onPrefChanged(pref) {
 | 
						|
    switch (pref) {
 | 
						|
      case NETWORK_TRR_URI_PREF:
 | 
						|
      case NETWORK_TRR_MODE_PREF:
 | 
						|
        Preferences.set(DISABLED_PREF, true);
 | 
						|
        await this.disableHeuristics("manuallyDisabled");
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // Connection change events are debounced to allow the network to settle.
 | 
						|
  // We wait for the network to be up for a period of kDebounceTimeout before
 | 
						|
  // handling the change. The timer is canceled when the network goes down and
 | 
						|
  // restarted the first time we learn that it went back up.
 | 
						|
  _debounceTimer: null,
 | 
						|
  _cancelDebounce() {
 | 
						|
    if (!this._debounceTimer) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    clearTimeout(this._debounceTimer);
 | 
						|
    this._debounceTimer = null;
 | 
						|
  },
 | 
						|
 | 
						|
  _lastDebounceTimestamp: 0,
 | 
						|
  onConnectionChanged() {
 | 
						|
    if (!gNetworkLinkService.isLinkUp) {
 | 
						|
      // Network is down - reset debounce timer.
 | 
						|
      this._cancelDebounce();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._debounceTimer) {
 | 
						|
      // Already debouncing - nothing to do.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._lastDebounceTimestamp = Date.now();
 | 
						|
    this._debounceTimer = setTimeout(() => {
 | 
						|
      this._cancelDebounce();
 | 
						|
      this.onConnectionChangedDebounced();
 | 
						|
    }, kDebounceTimeout);
 | 
						|
  },
 | 
						|
 | 
						|
  async onConnectionChangedDebounced() {
 | 
						|
    if (!gNetworkLinkService.isLinkUp) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (gCaptivePortalService.state == gCaptivePortalService.LOCKED_PORTAL) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // The network is up and we don't know that we're in a locked portal.
 | 
						|
    // Run heuristics. If we detect a portal later, we'll run heuristics again
 | 
						|
    // when it's unlocked. In that case, this run will likely have failed.
 | 
						|
    await this.runHeuristics("netchange");
 | 
						|
  },
 | 
						|
 | 
						|
  async onConnectivityAvailable() {
 | 
						|
    if (this._debounceTimer) {
 | 
						|
      // Already debouncing - nothing to do.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.runHeuristics("connectivity");
 | 
						|
  },
 | 
						|
};
 |