tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

NetworkErrorLogging.sys.mjs (13957B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 function policyExpired(policy) {
      6  let currentDate = new Date();
      7  return (currentDate - policy.creation) / 1_000 > policy.nel.max_age;
      8 }
      9 
     10 function errorType(aChannel) {
     11  // TODO: we have to map a lot more error codes
     12  switch (aChannel.status) {
     13    case Cr.NS_ERROR_UNKNOWN_HOST:
     14      // TODO: if there is no connectivity, return "dns.unreachable"
     15      return "dns.name_not_resolved";
     16    case Cr.NS_ERROR_REDIRECT_LOOP:
     17      return "http.response.redirect_loop";
     18    case Cr.NS_BINDING_REDIRECTED:
     19      return "ok";
     20    case Cr.NS_ERROR_NET_TIMEOUT:
     21      return "tcp.timed_out";
     22    case Cr.NS_ERROR_NET_RESET:
     23      return "tcp.reset";
     24    case Cr.NS_ERROR_CONNECTION_REFUSED:
     25      return "tcp.refused";
     26    default:
     27      break;
     28  }
     29 
     30  if (
     31    aChannel.status == Cr.NS_OK &&
     32    (aChannel.responseStatus / 100 == 2 || aChannel.responseStatus == 304)
     33  ) {
     34    return "ok";
     35  }
     36 
     37  if (
     38    aChannel.status == Cr.NS_OK &&
     39    aChannel.responseStatus >= 400 &&
     40    aChannel.responseStatus <= 599
     41  ) {
     42    return "http.error";
     43  }
     44  return "unknown" + aChannel.status;
     45 }
     46 
     47 function channelPhase(aChannel) {
     48  const NS_NET_STATUS_RESOLVING_HOST = 0x4b0003;
     49  const NS_NET_STATUS_RESOLVED_HOST = 0x4b000b;
     50  const NS_NET_STATUS_CONNECTING_TO = 0x4b0007;
     51  const NS_NET_STATUS_CONNECTED_TO = 0x4b0004;
     52  const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c;
     53  const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d;
     54  const NS_NET_STATUS_SENDING_TO = 0x4b0005;
     55  const NS_NET_STATUS_WAITING_FOR = 0x4b000a;
     56  const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006;
     57  const NS_NET_STATUS_READING = 0x4b0008;
     58  const NS_NET_STATUS_WRITING = 0x4b0009;
     59 
     60  let lastStatus = aChannel.QueryInterface(
     61    Ci.nsIHttpChannelInternal
     62  ).lastTransportStatus;
     63 
     64  switch (lastStatus) {
     65    case NS_NET_STATUS_RESOLVING_HOST:
     66    case NS_NET_STATUS_RESOLVED_HOST:
     67      return "dns";
     68    case NS_NET_STATUS_CONNECTING_TO:
     69    case NS_NET_STATUS_CONNECTED_TO: // TODO: is this right?
     70      return "connection";
     71    case NS_NET_STATUS_TLS_HANDSHAKE_STARTING:
     72    case NS_NET_STATUS_TLS_HANDSHAKE_ENDED:
     73      return "connection";
     74    case NS_NET_STATUS_SENDING_TO:
     75    case NS_NET_STATUS_WAITING_FOR:
     76    case NS_NET_STATUS_RECEIVING_FROM:
     77    case NS_NET_STATUS_READING:
     78    case NS_NET_STATUS_WRITING:
     79      return "application";
     80    default:
     81      // XXX(valentin): we default to DNS, but we should never get here.
     82      return "dns";
     83  }
     84 }
     85 
     86 export class NetworkErrorLogging {
     87  constructor() {}
     88 
     89  // Policy cache
     90  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#policy-cache
     91  policyCache = {};
     92  // TODO: maybe persist policies to disk?
     93 
     94  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#process-policy-headers
     95  registerPolicy(aChannel) {
     96    // 1. Abort these steps if any of the following conditions are true:
     97    // 1.1 The result of executing the "Is origin potentially trustworthy?" algorithm on request's origin is not Potentially Trustworthy.
     98    if (
     99      !Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
    100        .isOriginPotentiallyTrustworthy
    101    ) {
    102      return;
    103    }
    104 
    105    // 4. Let header be the value of the response header whose name is NEL.
    106    // 5. Let list be the result of executing the algorithm defined in Section 4 of [HTTP-JFV] on header. If that algorithm results in an error, or if list is empty, abort these steps.
    107    let list = [];
    108    aChannel.getOriginalResponseHeader("NEL", {
    109      QueryInterface: ChromeUtils.generateQI(["nsIHttpHeaderVisitor"]),
    110      visitHeader: (aHeader, aValue) => {
    111        list.push(aValue);
    112        // We only care about the first one so we could exit early
    113        // We could throw early, but that makes the errors show up in stderr.
    114        // The performance impact of not throwing is minimal.
    115        // throw new Error(Cr.NS_ERROR_ABORT);
    116      },
    117    });
    118 
    119    // 1.2 response does not contain a response header whose name is NEL.
    120    if (!list.length) {
    121      return;
    122    }
    123 
    124    // 2. Let origin be request's origin.
    125    let origin =
    126      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;
    127 
    128    // 3. Let key be the result of calling determine the network partition key, given request.
    129    let key = Services.io.originAttributesForNetworkState(aChannel);
    130 
    131    // 6. Let item be the first element of list.
    132    let item = JSON.parse(list[0]);
    133 
    134    // 7. If item has no member named max_age, or that member's value is not a number, abort these steps.
    135    if (!item.max_age || !Number.isInteger(item.max_age)) {
    136      return;
    137    }
    138 
    139    // 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.
    140    if (!item.max_age) {
    141      delete this.policyCache[String([key, origin])];
    142      return;
    143    }
    144 
    145    // 9. If item has no member named report_to, or that member's value is not a string, abort these steps.
    146    if (!item.report_to || typeof item.report_to != "string") {
    147      return;
    148    }
    149 
    150    // 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.
    151    if (
    152      item.success_fraction &&
    153      (typeof item.success_fraction != "number" ||
    154        item.success_fraction < 0 ||
    155        item.success_fraction > 1)
    156    ) {
    157      return;
    158    }
    159 
    160    // 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.
    161    if (
    162      item.failure_fraction &&
    163      (typeof item.failure_fraction != "number" ||
    164        item.failure_fraction < 0 ||
    165        item.success_fraction > 1)
    166    ) {
    167      return;
    168    }
    169 
    170    // 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.
    171    if (
    172      item.request_headers &&
    173      !Array.isArray(
    174        item.request_headers ||
    175          !item.request_headers.every(e => typeof e == "string")
    176      )
    177    ) {
    178      return;
    179    }
    180 
    181    // 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.
    182    if (
    183      item.response_headers &&
    184      !Array.isArray(
    185        item.response_headers ||
    186          !item.response_headers.every(e => typeof e == "string")
    187      )
    188    ) {
    189      return;
    190    }
    191 
    192    // 14. Let policy be a new NEL policy whose properties are set as follows:
    193    let policy = {};
    194 
    195    // received IP address
    196    // XXX: What should we do when using a proxy?
    197    try {
    198      policy.ip_address = aChannel.QueryInterface(
    199        Ci.nsIHttpChannelInternal
    200      ).remoteAddress;
    201    } catch (e) {
    202      return;
    203    }
    204 
    205    // origin
    206    policy.origin = origin;
    207 
    208    if (item.include_subdomains) {
    209      policy.subdomains = true;
    210    }
    211 
    212    policy.request_headers = item.request_headers;
    213    policy.response_headers = item.response_headers;
    214    policy.ttl = item.max_age;
    215    policy.creation = new Date();
    216    policy.successful_sampling_rate = item.success_fraction || 0.0;
    217    policy.failure_sampling_rate = item.failure_fraction || 1.0;
    218 
    219    policy.nel = item;
    220 
    221    // 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).
    222    this.policyCache[String([key, origin])] = policy;
    223  }
    224 
    225  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#choose-a-policy-for-a-request
    226  choosePolicyForRequest(aChannel) {
    227    // 1. Let origin be request's origin.
    228    let principal =
    229      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel);
    230    let origin = principal.origin;
    231    // 2. Let key be the result of calling determine the network partition key, given request.
    232    let key = Services.io.originAttributesForNetworkState(aChannel);
    233 
    234    // 3. If there is an entry in the policy cache for (key, origin):
    235    let policy = this.policyCache[String([key, origin])];
    236    //   3.1. Let policy be that entry.
    237    if (policy) {
    238      // 3.2. If policy is not expired, return it.
    239      if (!policyExpired(policy)) {
    240        return { policy, key, origin };
    241      }
    242    }
    243 
    244    // 4. For each parent origin that is a superdomain match of origin:
    245    // 4.1. If there is an entry in the policy cache for (key, parent origin):
    246    //    4.1.1. Let policy be that entry.
    247    //    4.1.2. If policy is not expired, and its subdomains flag is include, return it.
    248    while (principal.nextSubDomainPrincipal) {
    249      principal = principal.nextSubDomainPrincipal;
    250      origin = principal.origin;
    251      policy = this.policyCache[String([key, origin])];
    252      if (policy && !policyExpired(policy)) {
    253        return { policy, key, origin };
    254      }
    255    }
    256 
    257    // 5. Return no policy.
    258    return {};
    259  }
    260 
    261  // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#generate-a-network-error-report
    262  generateNELReport(aChannel) {
    263    // 1. If the result of executing the "Is origin potentially trustworthy?" algorithm on request's origin is not Potentially Trustworthy, return null.
    264    if (
    265      !Services.scriptSecurityManager.getChannelResultPrincipal(aChannel)
    266        .isOriginPotentiallyTrustworthy
    267    ) {
    268      return null;
    269    }
    270    // 2. Let origin be request's origin.
    271    let origin =
    272      Services.scriptSecurityManager.getChannelResultPrincipal(aChannel).origin;
    273 
    274    // 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.
    275    let {
    276      policy,
    277      key,
    278      origin: policyOrigin,
    279    } = this.choosePolicyForRequest(aChannel);
    280    if (!policy) {
    281      return null;
    282    }
    283 
    284    // 4. Determine the active sampling rate for this request:
    285    let samplingRate = 0.0;
    286    if (
    287      aChannel.status == Cr.NS_OK &&
    288      aChannel.responseStatus >= 200 &&
    289      aChannel.responseStatus <= 299
    290    ) {
    291      // If request succeeded, let sampling rate be policy's successful sampling rate.
    292      samplingRate = policy.successful_sampling_rate || 0.0;
    293    } else {
    294      // If request failed, let sampling rate be policy's failure sampling rate.
    295      samplingRate = policy.successful_sampling_rate || 1.0;
    296    }
    297 
    298    // 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.
    299    if (Math.random() >= samplingRate) {
    300      return null;
    301    }
    302 
    303    // 6. Let report body be a new ECMAScript object with the following properties:
    304 
    305    let phase = channelPhase(aChannel);
    306    let report_body = {
    307      sampling_fraction: samplingRate,
    308      elapsed_time: 1, // TODO
    309      phase,
    310      type: errorType(aChannel), // TODO
    311    };
    312 
    313    // 7. If report body's phase property is not dns, append the following properties to report body:
    314    if (phase != "dns") {
    315      // XXX: should we actually report server_ip?
    316      // It could be used to detect the presence of a PiHole.
    317      report_body.server_ip = aChannel.QueryInterface(
    318        Ci.nsIHttpChannelInternal
    319      ).remoteAddress;
    320      report_body.protocol = aChannel.protocolVersion;
    321    }
    322 
    323    // 8. If report body's phase property is not dns or connection, append the following properties to report body:
    324    // referrer?
    325    // method
    326    // request_headers?
    327    // response_headers?
    328    // status_code
    329    if (phase != "dns" && phase != "connection") {
    330      report_body.method = aChannel.requestMethod;
    331      report_body.status_code = aChannel.responseStatus;
    332    }
    333 
    334    // 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.
    335    if (
    336      origin != policyOrigin &&
    337      policy.subdomains &&
    338      report_body.phase != "dns"
    339    ) {
    340      return null;
    341    }
    342 
    343    // 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:
    344    if (phase != "dns" && report_body.server_ip != policy.ip_address) {
    345      // 10.1 Set report body's phase to dns.
    346      report_body.phase = "dns";
    347      // 10.2 Set report body's type to dns.address_changed.
    348      report_body.type = "dns.address_changed";
    349      // 10.3 Clear report body's request_headers, response_headers, status_code, and elapsed_time properties.
    350      delete report_body.request_headers;
    351      delete report_body.response_headers;
    352      delete report_body.status_code;
    353      delete report_body.elapsed_time;
    354    }
    355 
    356    // 11. If policy is stale, then delete policy from the policy cache.
    357    let currentDate = new Date();
    358    if ((currentDate - policy.creation) / 1_000 > 172800) {
    359      // Delete the policy.
    360      delete this.policyCache[String([key, policyOrigin])];
    361 
    362      // XXX: should we exit here, or continue submit the report?
    363    }
    364 
    365    // 12. Return report body and policy.
    366 
    367    // https://www.w3.org/TR/2023/WD-network-error-logging-20231005/#deliver-a-network-report
    368    // 1. Let url be request's URL.
    369    // 2. Clear url's fragment.
    370    let uriMutator = aChannel.URI.mutate().setRef("");
    371    // 3. If report body's phase property is dns or connection:
    372    //    Clear url's path and query.
    373    if (report_body.phase == "dns" || report_body.phase == "connection") {
    374      uriMutator.setPathQueryRef("");
    375    }
    376    if (aChannel.URI.hasUserPass) {
    377      uriMutator.setUserPass("");
    378    }
    379    let url = uriMutator.finalize().spec;
    380 
    381    // 4. Generate a network report given these parameters:
    382    // nsINetworkErrorReport
    383    let retObj = {
    384      body: JSON.stringify(report_body),
    385      group: policy.nel.report_to,
    386      url,
    387    };
    388 
    389    // nsHttpChannel will call ReportDeliver::Fetch
    390    return retObj;
    391  }
    392 
    393  QueryInterface = ChromeUtils.generateQI(["nsINetworkErrorLogging"]);
    394 }