bout the first one so we could exit early
        // We could throw early, but that makes the errors show up in stderr.
        // The performance impact of not throwing is minimal.
        // throw new Error(Cr.NS_ERROR_ABORT);
      },
    });

    // 1.2 response does not contain a response header whose name is NEL.
    if (!list.length) {
      return;
    }

    // 2. Let origin be request's origin.
    let origin =
      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;

    // 3. Let key be the result of calling determine the network partition key, given request.
    let key = Services.io.originAttributesForNetworkState(aChannel);

    // 6. Let item be the first element of list.
    let item = JSON.parse(list[0]);

    // 7. If item has no member named max_age, or that member's value is not a number, abort these steps.
    if (!item.max_age || !Number.isInteger(item.max_age)) {
      return;
    }

    // 8. If the value of item's max_age member is 0, then remove any NEL policy from the policy cache whose origin is origin, and skip the remaining steps.
    if (!item.max_age) {
      delete this.policyCache[String([key, origin])];
      return;
    }

    // 9. If item has no member named report_to, or that member's value is not a string, abort these steps.
    if (!item.report_to || typeof item.report_to != "string") {
      return;
    }

    // 10. If item has a member named success_fraction, whose value is not a number in the range 0.0 to 1.0, inclusive, abort these steps.
    if (
      item.success_fraction &&
      (typeof item.success_fraction != "number" ||
        item.success_fraction < 0 ||
        item.success_fraction > 1)
    ) {
      return;
    }

    // 11. If item has a member named failure_fraction, whose value is not a number in the range 0.0 to 1.0, inclusive, abort these steps.
    if (
      item.failure_fraction &&
      (typeof item.failure_fraction != "number" ||
        item.failure_fraction < 0 ||
        item.success_fraction > 1)
    ) {
      return;
    }

    // 12. If item has a member named request_headers, whose value is not a list, or if any element of that list is not a string, abort these steps.
    if (
      item.request_headers &&
      !Array.isArray(
        item.request_headers ||
          !item.request_headers.every(e => typeof e == "string")
      )
    ) {
      return;
    }

    // 13. If item has a member named response_headers, whose value is not a list, or if any element of that list is not a string, abort these steps.
    if (
      item.response_headers &&
      !Array.isArray(
        item.response_headers ||
          !item.response_headers.every(e => typeof e == "string")
      )
    ) {
      return;
    }

    // 14. Let policy be a new NEL policy whose properties are set as follows:
    let policy = {};

    // received IP address
    // XXX: What should we do when using a proxy?
    try {
      policy.ip_address = aChannel.QueryInterface(
        Ci.nsIHttpChannelInternal
      ).remoteAddress;
    } catch (e) {
      return;
    }

    // origin
    policy.origin = origin;

    if (item.include_subdomains) {
      policy.subdomains = true;
    }

    policy.request_headers = item.request_headers;
    policy.response_headers = item.response_headers;
    policy.ttl = item.max_age;
    policy.creation = new Date();
    policy.successful_sampling_rate = item.success_fraction || 0.0;
    policy.failure_sampling_rate = item.failure_fraction || 1.0;

    policy.nel = item;

    // 15. If there is already an entry in the policy cache for (key, origin), replace it with policy; otherwise, insert policy into the policy cache for (key, origin).
    this.policyCache[String([key, origin])] = policy;
  }

  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#choose-a-policy-for-a-request
  choosePolicyForRequest(aChannel) {
    // 1. Let origin be request's origin.
    let principal =
      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel);
    let origin = principal.origin;
    // 2. Let key be the result of calling determine the network partition key, given request.
    let key = Services.io.originAttributesForNetworkState(aChannel);

    // 3. If there is an entry in the policy cache for (key, origin):
    let policy = this.policyCache[String([key, origin])];
    //   3.1. Let policy be that entry.
    if (policy) {
      // 3.2. If policy is not expired, return it.
      if (!policyExpired(policy)) {
        return { policy, key, origin };
      }
    }

    // 4. For each parent origin that is a superdomain match of origin:
    // 4.1. If there is an entry in the policy cache for (key, parent origin):
    //    4.1.1. Let policy be that entry.
    //    4.1.2. If policy is not expired, and its subdomains flag is include, return it.
    while (principal.nextSubDomainPrincipal) {
      principal = principal.nextSubDomainPrincipal;
      origin = principal.origin;
      policy = this.policyCache[String([key, origin])];
      if (policy && !policyExpired(policy)) {
        return { policy, key, origin };
      }
    }

    // 5. Return no policy.
    return {};
  }

  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#generate-a-network-error-report
  generateNELReport(aChannel) {
    // 1. If the result of executing the "Is origin potentially trustworthy?" algorithm on request's origin is not Potentially Trustworthy, return null.
    if (
      !Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
        .isOriginPotentiallyTrustworthy
    ) {
      return null;
    }
    // 2. Let origin be request's origin.
    let origin =
      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;

    // 3. Let policy be the result of executing 5.1 Choose a policy for a request on request. If policy is no policy, return null.
    let {
      policy,
      key,
      origin: policyOrigin,
    } = this.choosePolicyForRequest(aChannel);
    if (!policy) {
      return null;
    }

    // 4. Determine the active sampling rate for this request:
    let samplingRate = 0.0;
    if (
      aChannel.status == Cr.NS_OK &&
      aChannel.responseStatus >= 200 &&
      aChannel.responseStatus <= 299
    ) {
      // If request succeeded, let sampling rate be policy's successful sampling rate.
      samplingRate = policy.successful_sampling_rate || 0.0;
    } else {
      // If request failed, let sampling rate be policy's failure sampling rate.
      samplingRate = policy.successful_sampling_rate || 1.0;
    }

    // 5. Decide whether or not to report on this request. Let roll be a random number between 0.0 and 1.0, inclusive. If roll ≥ sampling rate, return null.
    if (Math.random() >= samplingRate) {
      return null;
    }

    // 6. Let report body be a new ECMAScript object with the following properties:

    let phase = channelPhase(aChannel);
    let report_body = {
      sampling_fraction: samplingRate,
      elapsed_time: 1, // TODO
      phase,
      type: errorType(aChannel), // TODO
    };

    // 7. If report body's phase property is not dns, append the following properties to report body:
    if (phase != "dns") {
      // XXX: should we actually report server_ip?
      // It could be used to detect the presence of a PiHole.
      report_body.server_ip = aChannel.QueryInterface(
        Ci.nsIHttpChannelInternal
      ).remoteAddress;
      report_body.protocol = aChannel.protocolVersion;
    }

    // 8. If report body's phase property is not dns or connection, append the following properties to report body:
    // referrer?
    // method
    // request_headers?
    // response_headers?
    // status_code
    if (phase != "dns" && phase != "connection") {
      report_body.method = aChannel.requestMethod;
      report_body.status_code = aChannel.responseStatus;
    }

    // 9. If origin is not equal to policy's origin, policy's subdomains flag is include, and report body's phase property is not dns, return null.
    if (
      origin != policyOrigin &&
      policy.subdomains &&
      report_body.phase != "dns"
    ) {
      return null;
    }

    // 10. If report body's phase property is not dns, and report body's server_ip property is non-empty and not equal to policy's received IP address:
    if (phase != "dns" && report_body.server_ip != policy.ip_address) {
      // 10.1 Set report body's phase to dns.
      report_body.phase = "dns";
      // 10.2 Set report body's type to dns.address_changed.
      report_body.type = "dns.address_changed";
      // 10.3 Clear report body's request_headers, response_headers, status_code, and elapsed_time properties.
      delete report_body.request_headers;
      delete report_body.response_headers;
      delete report_body.status_code;
      delete report_body.elapsed_time;
    }

    // 11. If policy is stale, then delete policy from the policy cache.
    let currentDate = new Date();
    if ((currentDate - policy.creation) / 1_000 > 172800) {
      // Delete the policy.
      delete this.policyCache[String([key, policyOrigin])];

      // XXX: should we exit here, or continue submit the report?
    }

    // 12. Return report body and policy.

    // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#deliver-a-network-report
    // 1. Let url be request's URL.
    // 2. Clear url's fragment.
    let uriMutator = aChannel.URI.mutate().setRef("");
    // 3. If report body's phase property is dns or connection:
    //    Clear url's path and query.
    if (report_body.phase == "dns" || report_body.phase == "connection") {
      uriMutator.setPathQueryRef("");
    }
    if (aChannel.URI.hasUserPass) {
      uriMutator.setUserPass("");
    }
    let url = uriMutator.finalize().spec;

    // 4. Generate a network report given these parameters:
    // nsINetworkErrorReport
    let retObj = {
      body: JSON.stringify(report_body),
      group: policy.nel.report_to,
      url,
    };

    // nsHttpChannel will call ReportDeliver::Fetch
    return retObj;
  }

  QueryInterface = ChromeUtils.generateQI(["nsINetworkErrorLogging"]);
}
PK