tor-browser

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

NetworkHelper.sys.mjs (29249B)


      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 http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * Software License Agreement (BSD License)
      7 *
      8 * Copyright (c) 2007, Parakey Inc.
      9 * All rights reserved.
     10 *
     11 * Redistribution and use of this software in source and binary forms,
     12 * with or without modification, are permitted provided that the
     13 * following conditions are met:
     14 *
     15 * * Redistributions of source code must retain the above
     16 *   copyright notice, this list of conditions and the
     17 *   following disclaimer.
     18 *
     19 * * Redistributions in binary form must reproduce the above
     20 *   copyright notice, this list of conditions and the
     21 *   following disclaimer in the documentation and/or other
     22 *   materials provided with the distribution.
     23 *
     24 * * Neither the name of Parakey Inc. nor the names of its
     25 *   contributors may be used to endorse or promote products
     26 *   derived from this software without specific prior
     27 *   written permission of Parakey Inc.
     28 *
     29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     30 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     31 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
     32 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
     33 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
     34 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     35 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
     36 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     37 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
     38 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     39 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
     40 * OF THE POSSIBILITY OF SUCH DAMAGE.
     41 */
     42 
     43 /*
     44 * Creator:
     45 *  Joe Hewitt
     46 * Contributors
     47 *  John J. Barton (IBM Almaden)
     48 *  Jan Odvarko (Mozilla Corp.)
     49 *  Max Stepanov (Aptana Inc.)
     50 *  Rob Campbell (Mozilla Corp.)
     51 *  Hans Hillen (Paciello Group, Mozilla)
     52 *  Curtis Bartley (Mozilla Corp.)
     53 *  Mike Collins (IBM Almaden)
     54 *  Kevin Decker
     55 *  Mike Ratcliffe (Comartis AG)
     56 *  Hernan Rodríguez Colmeiro
     57 *  Austin Andrews
     58 *  Christoph Dorn
     59 *  Steven Roussey (AppCenter Inc, Network54)
     60 *  Mihai Sucan (Mozilla Corp.)
     61 */
     62 
     63 const lazy = {};
     64 
     65 ChromeUtils.defineESModuleGetters(
     66  lazy,
     67  {
     68    DevToolsInfaillibleUtils:
     69      "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs",
     70 
     71    NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     72  },
     73  { global: "contextual" }
     74 );
     75 
     76 // It would make sense to put this in the above
     77 // ChromeUtils.defineESModuleGetters, but that doesn't seem to work.
     78 ChromeUtils.defineLazyGetter(lazy, "certDecoder", () => {
     79  const { parse, pemToDER } = ChromeUtils.importESModule(
     80    "chrome://global/content/certviewer/certDecoder.mjs",
     81    { global: "contextual" }
     82  );
     83  return { parse, pemToDER };
     84 });
     85 
     86 // "Lax", "Strict" and "None" are special values of the SameSite cookie
     87 // attribute that should not be translated.
     88 const COOKIE_SAMESITE = {
     89  LAX: "Lax",
     90  STRICT: "Strict",
     91  NONE: "None",
     92 };
     93 
     94 /**
     95 * Helper object for networking stuff.
     96 *
     97 * Most of the following functions have been taken from the Firebug source. They
     98 * have been modified to match the Firefox coding rules.
     99 */
    100 export var NetworkHelper = {
    101  /**
    102   * Add charset to MIME type.
    103   *
    104   * @param string mimeType
    105   *        Initial MIME type.
    106   * @param string charset
    107   *        Charset to add to the MIME type.
    108   * @returns {string}
    109   *        Final MIME type.
    110   */
    111  addCharsetToMimeType(mimeType, charset) {
    112    if (mimeType && charset) {
    113      mimeType += "; charset=" + charset;
    114    }
    115 
    116    return mimeType;
    117  },
    118 
    119  /**
    120   * Converts text with a given charset to unicode.
    121   *
    122   * @param string text
    123   *        Text to convert.
    124   * @param string charset
    125   *        Charset to convert the text to.
    126   * @param boolean throwOnFailure
    127   *        Whether exceptions should be bubbled up or swallowed. Defaults to
    128   *        false.
    129   * @returns string
    130   *          Converted text.
    131   */
    132  convertToUnicode(text, charset, throwOnFailure) {
    133    // FIXME: We need to throw when text can't be converted e.g. the contents of
    134    // an image. Until we have a way to do so with TextEncoder and TextDecoder
    135    // we need to use nsIScriptableUnicodeConverter instead.
    136    const conv = Cc[
    137      "@mozilla.org/intl/scriptableunicodeconverter"
    138    ].createInstance(Ci.nsIScriptableUnicodeConverter);
    139    try {
    140      conv.charset = charset || "UTF-8";
    141      return conv.ConvertToUnicode(text);
    142    } catch (ex) {
    143      if (throwOnFailure) {
    144        throw ex;
    145      }
    146      return text;
    147    }
    148  },
    149 
    150  /**
    151   * Reads all available bytes from stream and converts them to charset.
    152   *
    153   * @param nsIInputStream stream
    154   * @param string charset
    155   * @returns string
    156   *          UTF-16 encoded string based on the content of stream and charset.
    157   */
    158  readAndConvertFromStream(stream, charset) {
    159    let text = null;
    160    try {
    161      text = lazy.NetUtil.readInputStreamToString(stream, stream.available());
    162      return this.convertToUnicode(text, charset);
    163    } catch (err) {
    164      return text;
    165    }
    166  },
    167 
    168  /**
    169   * Reads the post data from request.
    170   *
    171   * @param nsIHttpChannel request
    172   * @param string charset
    173   *        The content document charset, used when reading the POSTed data.
    174   *
    175   * @returns object or null
    176   *          Returns an object with the following properties:
    177   *            - {string|null} data: post data as string if it was possible to
    178   *              read from request, otherwise null.
    179   *            - {boolean} isDecodedAsText: if the data could be successfully read with
    180   *              the specified charset.
    181   *          Returns null if the channel does not implement nsIUploadChannel,
    182   *          and therefore cannot upload a data stream.
    183   */
    184  readPostDataFromRequest(request, charset) {
    185    if (!(request instanceof Ci.nsIUploadChannel)) {
    186      return null;
    187    }
    188    const iStream = request.uploadStream;
    189 
    190    let isSeekableStream = false;
    191    if (iStream instanceof Ci.nsISeekableStream) {
    192      isSeekableStream = true;
    193    }
    194 
    195    let prevOffset;
    196    if (isSeekableStream) {
    197      prevOffset = iStream.tell();
    198      iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
    199    }
    200 
    201    let data = null;
    202    let isDecodedAsText = true;
    203 
    204    // Read data from the stream.
    205    try {
    206      data = lazy.NetUtil.readInputStreamToString(iStream, iStream.available());
    207    } catch {
    208      // If we failed to read the stream, assume there is no valid request post
    209      // data to display.
    210      return null;
    211    }
    212 
    213    // Decode the data as text with the provided charset.
    214    try {
    215      data = this.convertToUnicode(data, charset, true);
    216    } catch (err) {
    217      isDecodedAsText = false;
    218    }
    219 
    220    // Seek locks the file, so seek to the beginning only if necko hasn't
    221    // read it yet, since necko doesn't seek to 0 before reading (at lest
    222    // not till 459384 is fixed).
    223    if (isSeekableStream && prevOffset == 0) {
    224      iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
    225    }
    226 
    227    return { data, isDecodedAsText };
    228  },
    229 
    230  /**
    231   * Gets the topFrameElement that is associated with request. This
    232   * works in single-process and multiprocess contexts. It may cross
    233   * the content/chrome boundary.
    234   *
    235   * @param nsIHttpChannel request
    236   * @returns Element|null
    237   *          The top frame element for the given request.
    238   */
    239  getTopFrameForRequest(request) {
    240    try {
    241      return this.getRequestLoadContext(request).topFrameElement;
    242    } catch (ex) {
    243      // request loadContext is not always available.
    244    }
    245    return null;
    246  },
    247 
    248  /**
    249   * Gets the nsIDOMWindow that is associated with request.
    250   *
    251   * @param nsIHttpChannel request
    252   * @returns nsIDOMWindow or null
    253   */
    254  getWindowForRequest(request) {
    255    try {
    256      return this.getRequestLoadContext(request).associatedWindow;
    257    } catch (ex) {
    258      // On some request notificationCallbacks and loadGroup are both null,
    259      // so that we can't retrieve any nsILoadContext interface.
    260      // Fallback on nsILoadInfo to try to retrieve the request's window.
    261      // (this is covered by test_network_get.html and its CSS request)
    262      return request.loadInfo.loadingDocument?.defaultView;
    263    }
    264  },
    265 
    266  /**
    267   * Gets the nsILoadContext that is associated with request.
    268   *
    269   * @param nsIHttpChannel request
    270   * @returns nsILoadContext or null
    271   */
    272  getRequestLoadContext(request) {
    273    try {
    274      if (request.loadInfo.workerAssociatedBrowsingContext) {
    275        return request.loadInfo.workerAssociatedBrowsingContext;
    276      }
    277    } catch (ex) {
    278      // Ignore.
    279    }
    280    try {
    281      return request.notificationCallbacks.getInterface(Ci.nsILoadContext);
    282    } catch (ex) {
    283      // Ignore.
    284    }
    285 
    286    try {
    287      return request.loadGroup.notificationCallbacks.getInterface(
    288        Ci.nsILoadContext
    289      );
    290    } catch (ex) {
    291      // Ignore.
    292    }
    293 
    294    return null;
    295  },
    296 
    297  /**
    298   * Loads the content of url from the cache.
    299   *
    300   * @param string url
    301   *        URL to load the cached content for.
    302   * @param string charset
    303   *        Assumed charset of the cached content. Used if there is no charset
    304   *        on the channel directly.
    305   * @param function callback
    306   *        Callback that is called with the loaded cached content if available
    307   *        or null if something failed while getting the cached content.
    308   */
    309  loadFromCache(url, charset, callback) {
    310    const channel = lazy.NetUtil.newChannel({
    311      uri: url,
    312      loadUsingSystemPrincipal: true,
    313    });
    314 
    315    // Ensure that we only read from the cache and not the server.
    316    channel.loadFlags =
    317      Ci.nsIRequest.LOAD_FROM_CACHE |
    318      Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
    319      Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
    320 
    321    lazy.NetUtil.asyncFetch(channel, (inputStream, statusCode, request) => {
    322      if (!Components.isSuccessCode(statusCode)) {
    323        callback(null);
    324        return;
    325      }
    326 
    327      // Try to get the encoding from the channel. If there is none, then use
    328      // the passed assumed charset.
    329      const requestChannel = request.QueryInterface(Ci.nsIChannel);
    330      const contentCharset = requestChannel.contentCharset || charset;
    331 
    332      // Read the content of the stream using contentCharset as encoding.
    333      callback(this.readAndConvertFromStream(inputStream, contentCharset));
    334    });
    335  },
    336 
    337  /**
    338   * Parse a raw Cookie header value.
    339   *
    340   * @param string header
    341   *        The raw Cookie header value.
    342   * @return array
    343   *         Array holding an object for each cookie. Each object holds the
    344   *         following properties: name and value.
    345   */
    346  parseCookieHeader(header) {
    347    const cookies = header.split(";");
    348    const result = [];
    349 
    350    cookies.forEach(function (cookie) {
    351      const equal = cookie.indexOf("=");
    352      const name = cookie.substr(0, equal);
    353      const value = cookie.substr(equal + 1);
    354      result.push({
    355        name: unescape(name.trim()),
    356        value: unescape(value.trim()),
    357      });
    358    });
    359 
    360    return result;
    361  },
    362 
    363  /**
    364   * Parse a raw Set-Cookie header value.
    365   *
    366   * @param array headers
    367   *        Array of raw Set-Cookie header values.
    368   * @return array
    369   *         Array holding an object for each cookie. Each object holds the
    370   *         following properties: name, value, secure (boolean), httpOnly
    371   *         (boolean), path, domain, samesite and expires (ISO date string).
    372   */
    373  parseSetCookieHeaders(headers) {
    374    function parseSameSiteAttribute(attribute) {
    375      attribute = attribute.toLowerCase();
    376      switch (attribute) {
    377        case COOKIE_SAMESITE.LAX.toLowerCase():
    378          return COOKIE_SAMESITE.LAX;
    379        case COOKIE_SAMESITE.STRICT.toLowerCase():
    380          return COOKIE_SAMESITE.STRICT;
    381        default:
    382          return COOKIE_SAMESITE.NONE;
    383      }
    384    }
    385 
    386    const cookies = [];
    387 
    388    for (const header of headers) {
    389      const rawCookies = header.split(/\r\n|\n|\r/);
    390 
    391      rawCookies.forEach(function (cookie) {
    392        const equal = cookie.indexOf("=");
    393        const name = unescape(cookie.substr(0, equal).trim());
    394        const parts = cookie.substr(equal + 1).split(";");
    395        const value = unescape(parts.shift().trim());
    396 
    397        cookie = { name, value };
    398 
    399        parts.forEach(function (part) {
    400          part = part.trim();
    401          if (part.toLowerCase() == "secure") {
    402            cookie.secure = true;
    403          } else if (part.toLowerCase() == "httponly") {
    404            cookie.httpOnly = true;
    405          } else if (part.indexOf("=") > -1) {
    406            const pair = part.split("=");
    407            pair[0] = pair[0].toLowerCase();
    408            if (pair[0] == "path" || pair[0] == "domain") {
    409              cookie[pair[0]] = pair[1];
    410            } else if (pair[0] == "samesite") {
    411              cookie[pair[0]] = parseSameSiteAttribute(pair[1]);
    412            } else if (pair[0] == "expires") {
    413              try {
    414                pair[1] = pair[1].replace(/-/g, " ");
    415                cookie.expires = new Date(pair[1]).toISOString();
    416              } catch (ex) {
    417                // Ignore.
    418              }
    419            }
    420          }
    421        });
    422 
    423        cookies.push(cookie);
    424      });
    425    }
    426 
    427    return cookies;
    428  },
    429 
    430  // This is a list of all the mime category maps jviereck could find in the
    431  // firebug code base.
    432  mimeCategoryMap: {
    433    "text/plain": "txt",
    434    "text/html": "html",
    435    "text/xml": "xml",
    436    "text/xsl": "txt",
    437    "text/xul": "txt",
    438    "text/css": "css",
    439    "text/sgml": "txt",
    440    "text/rtf": "txt",
    441    "text/x-setext": "txt",
    442    "text/richtext": "txt",
    443    "text/javascript": "js",
    444    "text/jscript": "txt",
    445    "text/tab-separated-values": "txt",
    446    "text/rdf": "txt",
    447    "text/xif": "txt",
    448    "text/ecmascript": "js",
    449    "text/vnd.curl": "txt",
    450    "text/x-json": "json",
    451    "text/x-js": "txt",
    452    "text/js": "txt",
    453    "text/vbscript": "txt",
    454    "view-source": "txt",
    455    "view-fragment": "txt",
    456    "application/xml": "xml",
    457    "application/xhtml+xml": "xml",
    458    "application/atom+xml": "xml",
    459    "application/rss+xml": "xml",
    460    "application/vnd.mozilla.maybe.feed": "xml",
    461    "application/javascript": "js",
    462    "application/x-javascript": "js",
    463    "application/x-httpd-php": "txt",
    464    "application/rdf+xml": "xml",
    465    "application/ecmascript": "js",
    466    "application/http-index-format": "txt",
    467    "application/json": "json",
    468    "application/x-js": "txt",
    469    "application/x-mpegurl": "txt",
    470    "application/vnd.apple.mpegurl": "txt",
    471    "multipart/mixed": "txt",
    472    "multipart/x-mixed-replace": "txt",
    473    "image/svg+xml": "svg",
    474    "application/octet-stream": "bin",
    475    "image/jpeg": "image",
    476    "image/jpg": "image",
    477    "image/gif": "image",
    478    "image/png": "image",
    479    "image/bmp": "image",
    480    "application/x-shockwave-flash": "flash",
    481    "video/x-flv": "flash",
    482    "audio/mpeg3": "media",
    483    "audio/x-mpeg-3": "media",
    484    "video/mpeg": "media",
    485    "video/x-mpeg": "media",
    486    "video/vnd.mpeg.dash.mpd": "xml",
    487    "audio/ogg": "media",
    488    "application/ogg": "media",
    489    "application/x-ogg": "media",
    490    "application/x-midi": "media",
    491    "audio/midi": "media",
    492    "audio/x-mid": "media",
    493    "audio/x-midi": "media",
    494    "music/crescendo": "media",
    495    "audio/wav": "media",
    496    "audio/x-wav": "media",
    497    "text/json": "json",
    498    "application/x-json": "json",
    499    "application/json-rpc": "json",
    500    "application/x-web-app-manifest+json": "json",
    501    "application/manifest+json": "json",
    502  },
    503 
    504  /**
    505   * Check if the given MIME type is a text-only MIME type.
    506   *
    507   * @param string mimeType
    508   * @return boolean
    509   */
    510  isTextMimeType(mimeType) {
    511    if (mimeType.indexOf("text/") == 0) {
    512      return true;
    513    }
    514 
    515    // XML and JSON often come with custom MIME types, so in addition to the
    516    // standard "application/xml" and "application/json", we also look for
    517    // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and
    518    // "-json" as suffixes.
    519    if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) {
    520      return true;
    521    }
    522 
    523    const category = this.mimeCategoryMap[mimeType] || null;
    524    switch (category) {
    525      case "txt":
    526      case "js":
    527      case "json":
    528      case "css":
    529      case "html":
    530      case "svg":
    531      case "xml":
    532        return true;
    533 
    534      default:
    535        return false;
    536    }
    537  },
    538 
    539  /**
    540   * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
    541   * extracts security information from them.
    542   *
    543   * @param object securityInfo
    544   *        The securityInfo object of a request. If null channel is assumed
    545   *        to be insecure.
    546   * @param object originAttributes
    547   *        The OriginAttributes of the request.
    548   * @param object httpActivity
    549   *        The httpActivity object for the request with at least members
    550   *        { private, hostname }.
    551   * @param Map decodedCertificateCache
    552   *        A Map of certificate fingerprints to decoded certificates, to avoid
    553   *        repeatedly decoding previously-seen certificates.
    554   *
    555   * @return object
    556   *         Returns an object containing following members:
    557   *          - state: The security of the connection used to fetch this
    558   *                   request. Has one of following string values:
    559   *                    * "insecure": the connection was not secure (only http)
    560   *                    * "weak": the connection has minor security issues
    561   *                    * "broken": secure connection failed (e.g. expired cert)
    562   *                    * "secure": the connection was properly secured.
    563   *          If state == broken:
    564   *            - errorMessage: error code string.
    565   *          If state == secure:
    566   *            - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
    567   *            - cipherSuite: the cipher suite used in this connection.
    568   *            - cert: information about certificate used in this connection.
    569   *                    See parseCertificateInfo for the contents.
    570   *            - hsts: true if host uses Strict Transport Security,
    571   *                    false otherwise
    572   *            - hpkp: true if host uses Public Key Pinning, false otherwise
    573   *          If state == weak: Same as state == secure and
    574   *            - weaknessReasons: list of reasons that cause the request to be
    575   *                               considered weak. See getReasonsForWeakness.
    576   */
    577  async parseSecurityInfo(
    578    securityInfo,
    579    originAttributes,
    580    httpActivity,
    581    decodedCertificateCache
    582  ) {
    583    const info = {
    584      state: "insecure",
    585    };
    586 
    587    // The request did not contain any security info.
    588    if (!securityInfo) {
    589      return info;
    590    }
    591 
    592    /**
    593     * Different scenarios to consider here and how they are handled:
    594     * - request is HTTP, the connection is not secure
    595     *   => securityInfo is null
    596     *      => state === "insecure"
    597     *
    598     * - request is HTTPS, the connection is secure
    599     *   => .securityState has STATE_IS_SECURE flag
    600     *      => state === "secure"
    601     *
    602     * - request is HTTPS, the connection has security issues
    603     *   => .securityState has STATE_IS_INSECURE flag
    604     *   => .errorCode is an NSS error code.
    605     *      => state === "broken"
    606     *
    607     * - request is HTTPS, the connection was terminated before the security
    608     *   could be validated
    609     *   => .securityState has STATE_IS_INSECURE flag
    610     *   => .errorCode is NOT an NSS error code.
    611     *   => .errorMessage is not available.
    612     *      => state === "insecure"
    613     *
    614     * - request is HTTPS but it uses a weak cipher or old protocol, see
    615     *   https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
    616     *   security/manager/ssl/nsNSSCallbacks.cpp#l1233
    617     * - request is mixed content (which makes no sense whatsoever)
    618     *   => .securityState has STATE_IS_BROKEN flag
    619     *   => .errorCode is NOT an NSS error code
    620     *   => .errorMessage is not available
    621     *      => state === "weak"
    622     */
    623 
    624    const wpl = Ci.nsIWebProgressListener;
    625    const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
    626      Ci.nsINSSErrorsService
    627    );
    628    if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
    629      const state = securityInfo.securityState;
    630 
    631      let uri = null;
    632      if (httpActivity.channel?.URI) {
    633        uri = httpActivity.channel.URI;
    634      }
    635      if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
    636        // it is not enough to look at the transport security info -
    637        // schemes other than https and wss are subject to
    638        // downgrade/etc at the scheme level and should always be
    639        // considered insecure
    640        info.state = "insecure";
    641      } else if (state & wpl.STATE_IS_SECURE) {
    642        // The connection is secure if the scheme is sufficient
    643        info.state = "secure";
    644      } else if (state & wpl.STATE_IS_BROKEN) {
    645        // The connection is not secure, there was no error but there's some
    646        // minor security issues.
    647        info.state = "weak";
    648        info.weaknessReasons = this.getReasonsForWeakness(state);
    649      } else if (state & wpl.STATE_IS_INSECURE) {
    650        // This was most likely an https request that was aborted before
    651        // validation. Return info as info.state = insecure.
    652        return info;
    653      } else {
    654        lazy.DevToolsInfaillibleUtils.reportException(
    655          "NetworkHelper.parseSecurityInfo",
    656          "Security state " + state + " has no known STATE_IS_* flags."
    657        );
    658        return info;
    659      }
    660 
    661      // Cipher suite.
    662      info.cipherSuite = securityInfo.cipherName;
    663 
    664      // Key exchange group name.
    665      info.keaGroupName = securityInfo.keaGroupName;
    666 
    667      // Certificate signature scheme.
    668      info.signatureSchemeName = securityInfo.signatureSchemeName;
    669 
    670      // Protocol version.
    671      info.protocolVersion = this.formatSecurityProtocol(
    672        securityInfo.protocolVersion
    673      );
    674 
    675      // Certificate.
    676      info.cert = await this.parseCertificateInfo(
    677        securityInfo.serverCert,
    678        decodedCertificateCache
    679      );
    680 
    681      // Certificate transparency status.
    682      info.certificateTransparency = securityInfo.certificateTransparencyStatus;
    683 
    684      // HSTS and HPKP if available.
    685      if (httpActivity.hostname) {
    686        const sss = Cc["@mozilla.org/ssservice;1"].getService(
    687          Ci.nsISiteSecurityService
    688        );
    689        const pkps = Cc[
    690          "@mozilla.org/security/publickeypinningservice;1"
    691        ].getService(Ci.nsIPublicKeyPinningService);
    692 
    693        if (!uri) {
    694          // isSecureURI only cares about the host, not the scheme.
    695          const host = httpActivity.hostname;
    696          uri = Services.io.newURI("https://" + host);
    697        }
    698 
    699        info.hsts = sss.isSecureURI(uri, originAttributes);
    700        info.hpkp = pkps.hostHasPins(uri);
    701      } else {
    702        lazy.DevToolsInfaillibleUtils.reportException(
    703          "NetworkHelper.parseSecurityInfo",
    704          "Could not get HSTS/HPKP status as hostname is not available."
    705        );
    706        info.hsts = false;
    707        info.hpkp = false;
    708      }
    709    } else {
    710      // The connection failed.
    711      info.state = "broken";
    712      info.errorMessage = securityInfo.errorCodeString;
    713    }
    714 
    715    // These values can be unset in rare cases, e.g. when stashed connection
    716    // data is deseralized from an older version of Firefox.
    717    try {
    718      info.usedEch = securityInfo.isAcceptedEch;
    719    } catch {
    720      info.usedEch = false;
    721    }
    722    try {
    723      info.usedDelegatedCredentials = securityInfo.isDelegatedCredential;
    724    } catch {
    725      info.usedDelegatedCredentials = false;
    726    }
    727    info.usedOcsp = securityInfo.madeOCSPRequests;
    728    info.usedPrivateDns = securityInfo.usedPrivateDNS;
    729 
    730    return info;
    731  },
    732 
    733  /**
    734   * Takes an nsIX509Cert and returns an object with certificate information.
    735   *
    736   * @param nsIX509Cert cert
    737   *        The certificate to extract the information from.
    738   * @param Map decodedCertificateCache
    739   *        A Map of certificate fingerprints to decoded certificates, to avoid
    740   *        repeatedly decoding previously-seen certificates.
    741   * @return object
    742   *         An object with following format:
    743   *           {
    744   *             subject: { commonName, organization, organizationalUnit },
    745   *             issuer: { commonName, organization, organizationUnit },
    746   *             validity: { start, end },
    747   *             fingerprint: { sha1, sha256 }
    748   *           }
    749   */
    750  async parseCertificateInfo(cert, decodedCertificateCache) {
    751    function getDNComponent(dn, componentType) {
    752      for (const [type, value] of dn.entries) {
    753        if (type == componentType) {
    754          return value;
    755        }
    756      }
    757      return undefined;
    758    }
    759 
    760    const info = {};
    761    if (cert) {
    762      const certHash = cert.sha256Fingerprint;
    763      let parsedCert = decodedCertificateCache.get(certHash);
    764      if (!parsedCert) {
    765        parsedCert = await lazy.certDecoder.parse(
    766          lazy.certDecoder.pemToDER(cert.getBase64DERString())
    767        );
    768        decodedCertificateCache.set(certHash, parsedCert);
    769      }
    770      info.subject = {
    771        commonName: getDNComponent(parsedCert.subject, "Common Name"),
    772        organization: getDNComponent(parsedCert.subject, "Organization"),
    773        organizationalUnit: getDNComponent(
    774          parsedCert.subject,
    775          "Organizational Unit"
    776        ),
    777      };
    778 
    779      info.issuer = {
    780        commonName: getDNComponent(parsedCert.issuer, "Common Name"),
    781        organization: getDNComponent(parsedCert.issuer, "Organization"),
    782        organizationUnit: getDNComponent(
    783          parsedCert.issuer,
    784          "Organizational Unit"
    785        ),
    786      };
    787 
    788      info.validity = {
    789        start: parsedCert.notBeforeUTC,
    790        end: parsedCert.notAfterUTC,
    791      };
    792 
    793      info.fingerprint = {
    794        sha1: parsedCert.fingerprint.sha1,
    795        sha256: parsedCert.fingerprint.sha256,
    796      };
    797    } else {
    798      lazy.DevToolsInfaillibleUtils.reportException(
    799        "NetworkHelper.parseCertificateInfo",
    800        "Secure connection established without certificate."
    801      );
    802    }
    803 
    804    return info;
    805  },
    806 
    807  /**
    808   * Takes protocolVersion of TransportSecurityInfo object and returns
    809   * human readable description.
    810   *
    811   * @param Number version
    812   *        One of nsITransportSecurityInfo version constants.
    813   * @return string
    814   *         One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version
    815   *         is valid, Unknown otherwise.
    816   */
    817  formatSecurityProtocol(version) {
    818    switch (version) {
    819      case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
    820        return "TLSv1";
    821      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
    822        return "TLSv1.1";
    823      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
    824        return "TLSv1.2";
    825      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
    826        return "TLSv1.3";
    827      default:
    828        lazy.DevToolsInfaillibleUtils.reportException(
    829          "NetworkHelper.formatSecurityProtocol",
    830          "protocolVersion " + version + " is unknown."
    831        );
    832        return "Unknown";
    833    }
    834  },
    835 
    836  /**
    837   * Takes the securityState bitfield and returns reasons for weak connection
    838   * as an array of strings.
    839   *
    840   * @param Number state
    841   *        nsITransportSecurityInfo.securityState.
    842   *
    843   * @return Array[String]
    844   *         List of weakness reasons. A subset of { cipher } where
    845   *         * cipher: The cipher suite is consireded to be weak (RC4).
    846   */
    847  getReasonsForWeakness(state) {
    848    const wpl = Ci.nsIWebProgressListener;
    849 
    850    // If there's non-fatal security issues the request has STATE_IS_BROKEN
    851    // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
    852    // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
    853    const reasons = [];
    854 
    855    if (state & wpl.STATE_IS_BROKEN) {
    856      const isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
    857 
    858      if (isCipher) {
    859        reasons.push("cipher");
    860      }
    861 
    862      if (!isCipher) {
    863        lazy.DevToolsInfaillibleUtils.reportException(
    864          "NetworkHelper.getReasonsForWeakness",
    865          "STATE_IS_BROKEN without a known reason. Full state was: " + state
    866        );
    867      }
    868    }
    869 
    870    return reasons;
    871  },
    872 
    873  /**
    874   * Parse a url's query string into its components
    875   *
    876   * @param string queryString
    877   *        The query part of a url
    878   * @return array
    879   *         Array of query params {name, value}
    880   */
    881  parseQueryString(queryString) {
    882    // Make sure there's at least one param available.
    883    // Be careful here, params don't necessarily need to have values, so
    884    // no need to verify the existence of a "=".
    885    if (!queryString) {
    886      return null;
    887    }
    888 
    889    // Turn the params string into an array containing { name: value } tuples.
    890    const paramsArray = queryString
    891      .replace(/^[?&]/, "")
    892      .split("&")
    893      .map(e => {
    894        const param = e.split("=");
    895        return {
    896          name: param[0]
    897            ? NetworkHelper.convertToUnicode(unescape(param[0]))
    898            : "",
    899          value: param[1]
    900            ? NetworkHelper.convertToUnicode(unescape(param[1]))
    901            : "",
    902        };
    903      });
    904 
    905    return paramsArray;
    906  },
    907 };