tor-browser

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

URIFixup.sys.mjs (41614B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * vim: sw=2 ts=2 sts=2 expandtab
      3 * This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /**
      8 * This component handles fixing up URIs, by correcting obvious typos and adding
      9 * missing schemes.
     10 * URI references:
     11 *   http://www.faqs.org/rfcs/rfc1738.html
     12 *   http://www.faqs.org/rfcs/rfc2396.html
     13 */
     14 
     15 // TODO (Bug 1641220) getFixupURIInfo has a complex logic, that likely could be
     16 // simplified, but the risk of regressing its behavior is high.
     17 /* eslint complexity: ["error", 46] */
     18 
     19 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     20 
     21 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     22 
     23 const lazy = {};
     24 
     25 XPCOMUtils.defineLazyServiceGetter(
     26  lazy,
     27  "externalProtocolService",
     28  "@mozilla.org/uriloader/external-protocol-service;1",
     29  Ci.nsIExternalProtocolService
     30 );
     31 
     32 XPCOMUtils.defineLazyServiceGetter(
     33  lazy,
     34  "defaultProtocolHandler",
     35  "@mozilla.org/network/protocol;1?name=default",
     36  Ci.nsIProtocolHandler
     37 );
     38 
     39 XPCOMUtils.defineLazyServiceGetter(
     40  lazy,
     41  "fileProtocolHandler",
     42  "@mozilla.org/network/protocol;1?name=file",
     43  Ci.nsIFileProtocolHandler
     44 );
     45 
     46 XPCOMUtils.defineLazyPreferenceGetter(
     47  lazy,
     48  "fixupSchemeTypos",
     49  "browser.fixup.typo.scheme",
     50  true
     51 );
     52 XPCOMUtils.defineLazyPreferenceGetter(
     53  lazy,
     54  "dnsFirstForSingleWords",
     55  "browser.fixup.dns_first_for_single_words",
     56  false
     57 );
     58 XPCOMUtils.defineLazyPreferenceGetter(
     59  lazy,
     60  "keywordEnabled",
     61  "keyword.enabled",
     62  true
     63 );
     64 XPCOMUtils.defineLazyPreferenceGetter(
     65  lazy,
     66  "alternateProtocol",
     67  "browser.fixup.alternate.protocol",
     68  "https"
     69 );
     70 
     71 XPCOMUtils.defineLazyPreferenceGetter(
     72  lazy,
     73  "dnsResolveFullyQualifiedNames",
     74  "browser.urlbar.dnsResolveFullyQualifiedNames",
     75  true
     76 );
     77 
     78 const {
     79  FIXUP_FLAG_NONE,
     80  FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
     81  FIXUP_FLAGS_MAKE_ALTERNATE_URI,
     82  FIXUP_FLAG_PRIVATE_CONTEXT,
     83  FIXUP_FLAG_FIX_SCHEME_TYPOS,
     84 } = Ci.nsIURIFixup;
     85 
     86 const COMMON_PROTOCOLS = ["http", "https", "file"];
     87 
     88 const HTTPISH = new Set(["http", "https"]);
     89 
     90 // Regex used to identify user:password tokens in url strings.
     91 // This is not a strict valid characters check, because we try to fixup this
     92 // part of the url too.
     93 ChromeUtils.defineLazyGetter(
     94  lazy,
     95  "userPasswordRegex",
     96  () => /^([a-z+.-]+:\/{0,3})*([^\/@]+@).+/i
     97 );
     98 
     99 // Regex used to identify the string that starts with port expression.
    100 ChromeUtils.defineLazyGetter(lazy, "portRegex", () => /^:\d{1,5}([?#/]|$)/);
    101 
    102 // Regex used to identify numbers.
    103 ChromeUtils.defineLazyGetter(lazy, "numberRegex", () => /^[0-9]+(\.[0-9]+)?$/);
    104 
    105 // Regex used to identify tab separated content (having at least 2 tabs).
    106 ChromeUtils.defineLazyGetter(lazy, "maxOneTabRegex", () => /^[^\t]*\t?[^\t]*$/);
    107 
    108 // Regex used to test if a string with a protocol might instead be a url
    109 // without a protocol but with a port:
    110 //
    111 //   <hostname>:<port> or
    112 //   <hostname>:<port>/
    113 //
    114 // Where <hostname> is a string of alphanumeric characters and dashes
    115 // separated by dots.
    116 // and <port> is a 5 or less digits. This actually breaks the rfc2396
    117 // definition of a scheme which allows dots in schemes.
    118 //
    119 // Note:
    120 //   People expecting this to work with
    121 //   <user>:<password>@<host>:<port>/<url-path> will be disappointed!
    122 //
    123 // Note: Parser could be a lot tighter, tossing out silly hostnames
    124 //       such as those containing consecutive dots and so on.
    125 ChromeUtils.defineLazyGetter(
    126  lazy,
    127  "possiblyHostPortRegex",
    128  () => /^[a-z0-9-]+(\.[a-z0-9-]+)*:[0-9]{1,5}([/?#]|$)/i
    129 );
    130 
    131 // Regex used to strip newlines.
    132 ChromeUtils.defineLazyGetter(lazy, "newLinesRegex", () => /[\r\n]/g);
    133 
    134 // Regex used to match a possible protocol.
    135 // This resembles the logic in Services.io.extractScheme, thus \t is admitted
    136 // and stripped later. We don't use Services.io.extractScheme because of
    137 // performance bottleneck caused by crossing XPConnect.
    138 ChromeUtils.defineLazyGetter(
    139  lazy,
    140  "possibleProtocolRegex",
    141  () => /^([a-z][a-z0-9.+\t-]*)(:|;)?(\/\/)?/i
    142 );
    143 
    144 // Regex used to match IPs. Note that these are not made to validate IPs, but
    145 // just to detect strings that look like an IP. They also skip protocol.
    146 // For IPv4 this also accepts a shorthand format with just 2 dots.
    147 ChromeUtils.defineLazyGetter(
    148  lazy,
    149  "IPv4LikeRegex",
    150  () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i
    151 );
    152 ChromeUtils.defineLazyGetter(
    153  lazy,
    154  "IPv6LikeRegex",
    155  () =>
    156    /^(?:[a-z+.-]+:\/*(?!\/))?\[(?:[0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?(?::\d+|\/)?/i
    157 );
    158 
    159 // Cache of known domains.
    160 ChromeUtils.defineLazyGetter(lazy, "knownDomains", () => {
    161  const branch = "browser.fixup.domainwhitelist.";
    162  let domains = new Set(
    163    Services.prefs
    164      .getChildList(branch)
    165      .filter(p => Services.prefs.getBoolPref(p, false))
    166      .map(p => p.substring(branch.length))
    167  );
    168  // Hold onto the observer to avoid it being GC-ed.
    169  domains._observer = {
    170    observe(subject, topic, data) {
    171      let domain = data.substring(branch.length);
    172      if (Services.prefs.getBoolPref(data, false)) {
    173        domains.add(domain);
    174      } else {
    175        domains.delete(domain);
    176      }
    177    },
    178    QueryInterface: ChromeUtils.generateQI([
    179      "nsIObserver",
    180      "nsISupportsWeakReference",
    181    ]),
    182  };
    183  Services.prefs.addObserver(branch, domains._observer, true);
    184  return domains;
    185 });
    186 
    187 // Cache of known suffixes.
    188 // This works differently from the known domains, because when we examine a
    189 // domain we can't tell how many dot-separated parts constitute the suffix.
    190 // We create a Map keyed by the last dotted part, containing a Set of
    191 // all the suffixes ending with that part:
    192 //   "two" => ["two"]
    193 //   "three" => ["some.three", "three"]
    194 // When searching we can restrict the linear scan based on the last part.
    195 // The ideal structure for this would be a Directed Acyclic Word Graph, but
    196 // since we expect this list to be small it's not worth the complication.
    197 ChromeUtils.defineLazyGetter(lazy, "knownSuffixes", () => {
    198  const branch = "browser.fixup.domainsuffixwhitelist.";
    199  let suffixes = new Map();
    200  let prefs = Services.prefs
    201    .getChildList(branch)
    202    .filter(p => Services.prefs.getBoolPref(p, false));
    203  for (let pref of prefs) {
    204    let suffix = pref.substring(branch.length);
    205    let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
    206    if (lastPart) {
    207      let entries = suffixes.get(lastPart);
    208      if (!entries) {
    209        entries = new Set();
    210        suffixes.set(lastPart, entries);
    211      }
    212      entries.add(suffix);
    213    }
    214  }
    215  // Hold onto the observer to avoid it being GC-ed.
    216  suffixes._observer = {
    217    observe(subject, topic, data) {
    218      let suffix = data.substring(branch.length);
    219      let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
    220      let entries = suffixes.get(lastPart);
    221      if (Services.prefs.getBoolPref(data, false)) {
    222        // Add the suffix.
    223        if (!entries) {
    224          entries = new Set();
    225          suffixes.set(lastPart, entries);
    226        }
    227        entries.add(suffix);
    228      } else if (entries) {
    229        // Remove the suffix.
    230        entries.delete(suffix);
    231        if (!entries.size) {
    232          suffixes.delete(lastPart);
    233        }
    234      }
    235    },
    236    QueryInterface: ChromeUtils.generateQI([
    237      "nsIObserver",
    238      "nsISupportsWeakReference",
    239    ]),
    240  };
    241  Services.prefs.addObserver(branch, suffixes._observer, true);
    242  return suffixes;
    243 });
    244 
    245 export function URIFixup() {}
    246 
    247 URIFixup.prototype = {
    248  get FIXUP_FLAG_NONE() {
    249    return FIXUP_FLAG_NONE;
    250  },
    251  get FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP() {
    252    return FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
    253  },
    254  get FIXUP_FLAGS_MAKE_ALTERNATE_URI() {
    255    return FIXUP_FLAGS_MAKE_ALTERNATE_URI;
    256  },
    257  get FIXUP_FLAG_PRIVATE_CONTEXT() {
    258    return FIXUP_FLAG_PRIVATE_CONTEXT;
    259  },
    260  get FIXUP_FLAG_FIX_SCHEME_TYPOS() {
    261    return FIXUP_FLAG_FIX_SCHEME_TYPOS;
    262  },
    263 
    264  getFixupURIInfo(uriString, fixupFlags = FIXUP_FLAG_NONE) {
    265    let isPrivateContext = fixupFlags & FIXUP_FLAG_PRIVATE_CONTEXT;
    266    let untrimmedURIString = uriString;
    267 
    268    // Eliminate embedded newlines, which single-line text fields now allow,
    269    // and cleanup the empty spaces and tabs that might be on each end.
    270    uriString = uriString.trim().replace(lazy.newLinesRegex, "");
    271 
    272    if (!uriString) {
    273      throw new Components.Exception(
    274        "Should pass a non-null uri",
    275        Cr.NS_ERROR_FAILURE
    276      );
    277    }
    278 
    279    let info = new URIFixupInfo(uriString);
    280 
    281    const { scheme, fixedSchemeUriString, fixupChangedProtocol } =
    282      extractScheme(uriString, fixupFlags);
    283    uriString = fixedSchemeUriString;
    284    info.fixupChangedProtocol = fixupChangedProtocol;
    285 
    286    if (scheme == "view-source") {
    287      let { preferredURI, postData } = fixupViewSource(uriString, fixupFlags);
    288      info.preferredURI = info.fixedURI = preferredURI;
    289      info.postData = postData;
    290      return info;
    291    }
    292 
    293    if (scheme.length < 2) {
    294      // Check if it is a file path. We skip most schemes because the only case
    295      // where a file path may look like having a scheme is "X:" on Windows.
    296      let fileURI = fileURIFixup(uriString);
    297      if (fileURI) {
    298        info.preferredURI = info.fixedURI = fileURI;
    299        info.fixupChangedProtocol = true;
    300        return info;
    301      }
    302    }
    303 
    304    const isCommonProtocol = COMMON_PROTOCOLS.includes(scheme);
    305 
    306    let canHandleProtocol =
    307      scheme &&
    308      (isCommonProtocol ||
    309        Services.io.getProtocolHandler(scheme) != lazy.defaultProtocolHandler ||
    310        this._isKnownExternalProtocol(scheme));
    311 
    312    if (
    313      canHandleProtocol ||
    314      // If it's an unknown handler and the given URL looks like host:port or
    315      // has a user:password we can't pass it to the external protocol handler.
    316      // We'll instead try fixing it with http later.
    317      (!lazy.possiblyHostPortRegex.test(uriString) &&
    318        !lazy.userPasswordRegex.test(uriString))
    319    ) {
    320      // Just try to create a URL out of it.
    321      try {
    322        info.fixedURI = makeURIWithFixedLocalHosts(uriString, fixupFlags);
    323      } catch (ex) {
    324        if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
    325          throw ex;
    326        }
    327      }
    328    }
    329 
    330    // We're dealing with a theoretically valid URI but we have no idea how to
    331    // load it. (e.g. "christmas:humbug")
    332    // It's more likely the user wants to search, and so we chuck this over to
    333    // their preferred search provider.
    334    // TODO (Bug 1588118): Should check FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
    335    // instead of FIXUP_FLAG_FIX_SCHEME_TYPOS.
    336    if (
    337      info.fixedURI &&
    338      lazy.keywordEnabled &&
    339      fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS &&
    340      scheme &&
    341      !canHandleProtocol
    342    ) {
    343      tryKeywordFixupForURIInfo(uriString, info, isPrivateContext);
    344    }
    345 
    346    if (info.fixedURI) {
    347      if (!info.preferredURI) {
    348        maybeSetAlternateFixedURI(info, fixupFlags);
    349        info.preferredURI = info.fixedURI;
    350      }
    351      fixupConsecutiveDotsHost(info);
    352      return info;
    353    }
    354 
    355    // Fix up protocol string before calling KeywordURIFixup, because
    356    // it cares about the hostname of such URIs.
    357    // Prune duff protocol schemes:
    358    //   ://totallybroken.url.com
    359    //   //shorthand.url.com
    360    let inputHadDuffProtocol =
    361      uriString.startsWith("://") || uriString.startsWith("//");
    362    if (inputHadDuffProtocol) {
    363      uriString = uriString.replace(/^:?\/\//, "");
    364    }
    365 
    366    let detectSpaceInCredentials = val => {
    367      // Only search the first 512 chars for performance reasons.
    368      let firstChars = val.slice(0, 512);
    369      if (!firstChars.includes("@")) {
    370        return false;
    371      }
    372      let credentials = firstChars.split("@")[0];
    373      return !credentials.includes("/") && /\s/.test(credentials);
    374    };
    375 
    376    // Avoid fixing up content that looks like tab-separated values.
    377    // Assume that 1 tab is accidental, but more than 1 implies this is
    378    // supposed to be tab-separated content.
    379    if (
    380      !isCommonProtocol &&
    381      lazy.maxOneTabRegex.test(uriString) &&
    382      !detectSpaceInCredentials(untrimmedURIString)
    383    ) {
    384      let uriWithProtocol = fixupURIProtocol(uriString, fixupFlags);
    385      if (uriWithProtocol) {
    386        info.fixedURI = uriWithProtocol;
    387        info.fixupChangedProtocol = true;
    388        info.schemelessInput = Ci.nsILoadInfo.SchemelessInputTypeSchemeless;
    389        maybeSetAlternateFixedURI(info, fixupFlags);
    390        info.preferredURI = info.fixedURI;
    391        // Check if it's a forced visit. The user can enforce a visit by
    392        // appending a slash, but the string must be in a valid uri format.
    393        if (uriString.endsWith("/")) {
    394          fixupConsecutiveDotsHost(info);
    395          return info;
    396        }
    397      }
    398    }
    399 
    400    // Handle "www.<something>" as a URI.
    401    const asciiHost = info.fixedURI?.asciiHost;
    402    if (
    403      asciiHost?.length > 4 &&
    404      asciiHost?.startsWith("www.") &&
    405      asciiHost?.lastIndexOf(".") == 3
    406    ) {
    407      return info;
    408    }
    409 
    410    // Memoize the public suffix check, since it may be expensive and should
    411    // only run once when necessary.
    412    let suffixInfo;
    413    function checkSuffix(i) {
    414      if (!suffixInfo) {
    415        suffixInfo = checkAndFixPublicSuffix(i);
    416      }
    417      return suffixInfo;
    418    }
    419 
    420    // See if it is a keyword and whether a keyword must be fixed up.
    421    if (
    422      lazy.keywordEnabled &&
    423      fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP &&
    424      !inputHadDuffProtocol &&
    425      !checkSuffix(info).suffix &&
    426      keywordURIFixup(uriString, info, isPrivateContext)
    427    ) {
    428      fixupConsecutiveDotsHost(info);
    429      return info;
    430    }
    431 
    432    if (
    433      info.fixedURI &&
    434      (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix)
    435    ) {
    436      fixupConsecutiveDotsHost(info);
    437      return info;
    438    }
    439 
    440    // If we still haven't been able to construct a valid URI, try to force a
    441    // keyword match.
    442    if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) {
    443      tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext);
    444    }
    445 
    446    if (!info.preferredURI) {
    447      // We couldn't salvage anything.
    448      throw new Components.Exception(
    449        "Couldn't build a valid uri",
    450        Cr.NS_ERROR_MALFORMED_URI
    451      );
    452    }
    453 
    454    fixupConsecutiveDotsHost(info);
    455    return info;
    456  },
    457 
    458  webNavigationFlagsToFixupFlags(href, navigationFlags) {
    459    try {
    460      Services.io.newURI(href);
    461      // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris.
    462      navigationFlags &=
    463        ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
    464    } catch (ex) {}
    465 
    466    let fixupFlags = FIXUP_FLAG_NONE;
    467    if (
    468      navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
    469    ) {
    470      fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
    471    }
    472    if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
    473      fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS;
    474    }
    475    return fixupFlags;
    476  },
    477 
    478  keywordToURI(keyword, isPrivateContext) {
    479    if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
    480      // There's no search service in the content process, thus all the calls
    481      // from it that care about keywords conversion should go through the
    482      // parent process.
    483      throw new Components.Exception(
    484        "Can't invoke URIFixup in the content process",
    485        Cr.NS_ERROR_NOT_AVAILABLE
    486      );
    487    }
    488    let info = new URIFixupInfo(keyword);
    489 
    490    // Strip leading "?" and leading/trailing spaces from aKeyword
    491    if (keyword.startsWith("?")) {
    492      keyword = keyword.substring(1);
    493    }
    494    keyword = keyword.trim();
    495 
    496    if (!Services.search.hasSuccessfullyInitialized) {
    497      return info;
    498    }
    499 
    500    // Try falling back to the search service's default search engine
    501    // We must use an appropriate search engine depending on the private
    502    // context.
    503    let engine = isPrivateContext
    504      ? Services.search.defaultPrivateEngine
    505      : Services.search.defaultEngine;
    506 
    507    // We allow default search plugins to specify alternate parameters that are
    508    // specific to keyword searches.
    509    let responseType = null;
    510    if (engine.supportsResponseType("application/x-moz-keywordsearch")) {
    511      responseType = "application/x-moz-keywordsearch";
    512    }
    513    let submission = engine.getSubmission(keyword, responseType);
    514    if (
    515      !submission ||
    516      // For security reasons (avoid redirecting to file, data, or other unsafe
    517      // protocols) we only allow fixup to http/https search engines.
    518      !HTTPISH.has(submission.uri.scheme)
    519    ) {
    520      throw new Components.Exception(
    521        "Invalid search submission uri",
    522        Cr.NS_ERROR_NOT_AVAILABLE
    523      );
    524    }
    525    let submissionPostDataStream = submission.postData;
    526    if (submissionPostDataStream) {
    527      info.postData = submissionPostDataStream;
    528    }
    529 
    530    info.keywordProviderName = engine.name;
    531    info.keywordAsSent = keyword;
    532    info.preferredURI = submission.uri;
    533    return info;
    534  },
    535 
    536  forceHttpFixup(uriString) {
    537    if (!uriString) {
    538      throw new Components.Exception(
    539        "Should pass a non-null uri",
    540        Cr.NS_ERROR_FAILURE
    541      );
    542    }
    543 
    544    let info = new URIFixupInfo(uriString);
    545    let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme(
    546      uriString,
    547      FIXUP_FLAG_FIX_SCHEME_TYPOS
    548    );
    549 
    550    if (!HTTPISH.has(scheme)) {
    551      throw new Components.Exception(
    552        "Scheme should be either http or https",
    553        Cr.NS_ERROR_FAILURE
    554      );
    555    }
    556 
    557    info.fixupChangedProtocol = fixupChangedProtocol;
    558    info.fixedURI = Services.io.newURI(fixedSchemeUriString);
    559 
    560    let host = info.fixedURI.host;
    561    if (!HTTPISH.has(host) && host != "localhost") {
    562      let modifiedHostname = maybeAddPrefixAndSuffix(host);
    563      updateHostAndScheme(info, modifiedHostname);
    564      info.preferredURI = info.fixedURI;
    565    }
    566 
    567    return info;
    568  },
    569 
    570  checkHost(uri, listener, originAttributes) {
    571    let { displayHost, asciiHost } = uri;
    572    if (!displayHost) {
    573      throw new Components.Exception(
    574        "URI must have displayHost",
    575        Cr.NS_ERROR_FAILURE
    576      );
    577    }
    578    if (!asciiHost) {
    579      throw new Components.Exception(
    580        "URI must have asciiHost",
    581        Cr.NS_ERROR_FAILURE
    582      );
    583    }
    584 
    585    let isIPv4Address = host => {
    586      let parts = host.split(".");
    587      if (parts.length != 4) {
    588        return false;
    589      }
    590      return parts.every(part => {
    591        let n = parseInt(part, 10);
    592        return n >= 0 && n <= 255;
    593      });
    594    };
    595 
    596    // Avoid showing fixup information if we're suggesting an IP. Note that
    597    // decimal representations of IPs are normalized to a 'regular'
    598    // dot-separated IP address by network code, but that only happens for
    599    // numbers that don't overflow. Longer numbers do not get normalized,
    600    // but still work to access IP addresses. So for instance,
    601    // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
    602    // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
    603    // While 2130706433 would get normalized by network, 1097347366913
    604    // does not, and we have to deal with both cases here:
    605    if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) {
    606      return;
    607    }
    608 
    609    // For dotless hostnames, we want to ensure this ends with a '.' but don't
    610    // want the . showing up in the UI if we end up notifying the user, so we
    611    // use a separate variable.
    612    let lookupName = displayHost;
    613    if (lazy.dnsResolveFullyQualifiedNames && !lookupName.includes(".")) {
    614      lookupName += ".";
    615    }
    616 
    617    Services.obs.notifyObservers(null, "uri-fixup-check-dns");
    618    Services.dns.asyncResolve(
    619      lookupName,
    620      Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    621      0,
    622      null,
    623      listener,
    624      Services.tm.mainThread,
    625      originAttributes
    626    );
    627  },
    628 
    629  isDomainKnown,
    630 
    631  _isKnownExternalProtocol(scheme) {
    632    if (AppConstants.platform == "android") {
    633      // On Android, externalProtocolHandlerExists ~always returns true (see
    634      // nsOSHelperAppService::OSProtocolHandlerExists). For now, this
    635      // preserves behavior from before bug 1966666.
    636      return false;
    637    }
    638    return lazy.externalProtocolService.externalProtocolHandlerExists(scheme);
    639  },
    640 
    641  classID: Components.ID("{c6cf88b7-452e-47eb-bdc9-86e3561648ef}"),
    642  QueryInterface: ChromeUtils.generateQI(["nsIURIFixup"]),
    643 };
    644 
    645 export function URIFixupInfo(originalInput = "") {
    646  this._originalInput = originalInput;
    647 }
    648 
    649 URIFixupInfo.prototype = {
    650  set consumer(consumer) {
    651    this._consumer = consumer || null;
    652  },
    653  get consumer() {
    654    return this._consumer || null;
    655  },
    656 
    657  set preferredURI(uri) {
    658    this._preferredURI = uri;
    659  },
    660  get preferredURI() {
    661    return this._preferredURI || null;
    662  },
    663 
    664  set fixedURI(uri) {
    665    this._fixedURI = uri;
    666  },
    667  get fixedURI() {
    668    return this._fixedURI || null;
    669  },
    670 
    671  set keywordProviderName(name) {
    672    this._keywordProviderName = name;
    673  },
    674  get keywordProviderName() {
    675    return this._keywordProviderName || "";
    676  },
    677 
    678  set keywordAsSent(keyword) {
    679    this._keywordAsSent = keyword;
    680  },
    681  get keywordAsSent() {
    682    return this._keywordAsSent || "";
    683  },
    684 
    685  set schemelessInput(changed) {
    686    this._schemelessInput = changed;
    687  },
    688  get schemelessInput() {
    689    return this._schemelessInput ?? Ci.nsILoadInfo.SchemelessInputTypeUnset;
    690  },
    691 
    692  set fixupChangedProtocol(changed) {
    693    this._fixupChangedProtocol = changed;
    694  },
    695  get fixupChangedProtocol() {
    696    return !!this._fixupChangedProtocol;
    697  },
    698 
    699  set fixupCreatedAlternateURI(changed) {
    700    this._fixupCreatedAlternateURI = changed;
    701  },
    702  get fixupCreatedAlternateURI() {
    703    return !!this._fixupCreatedAlternateURI;
    704  },
    705 
    706  set originalInput(input) {
    707    this._originalInput = input;
    708  },
    709  get originalInput() {
    710    return this._originalInput || "";
    711  },
    712 
    713  set postData(postData) {
    714    this._postData = postData;
    715  },
    716  get postData() {
    717    return this._postData || null;
    718  },
    719 
    720  classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"),
    721  QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]),
    722 };
    723 
    724 // Helpers
    725 
    726 /**
    727 * Implementation of isDomainKnown, so we don't have to go through the
    728 * service.
    729 *
    730 * @param {string} asciiHost
    731 * @returns {boolean} whether the domain is known
    732 */
    733 function isDomainKnown(asciiHost) {
    734  if (lazy.dnsFirstForSingleWords) {
    735    return true;
    736  }
    737  // Check if this domain is known as an actual
    738  // domain (which will prevent a keyword query)
    739  // Note that any processing of the host here should stay in sync with
    740  // code in the front-end(s) that set the pref.
    741  let lastDotIndex = asciiHost.lastIndexOf(".");
    742  if (lastDotIndex == asciiHost.length - 1) {
    743    asciiHost = asciiHost.substring(0, asciiHost.length - 1);
    744    lastDotIndex = asciiHost.lastIndexOf(".");
    745  }
    746  if (lazy.knownDomains.has(asciiHost.toLowerCase())) {
    747    return true;
    748  }
    749  // If there's no dot or only a leading dot we are done, otherwise we'll check
    750  // against the known suffixes.
    751  if (lastDotIndex <= 0) {
    752    return false;
    753  }
    754  // Don't use getPublicSuffix here, since the suffix is not in the PSL,
    755  // thus it couldn't tell if the suffix is made up of one or multiple
    756  // dot-separated parts.
    757  let lastPart = asciiHost.substr(lastDotIndex + 1);
    758  let suffixes = lazy.knownSuffixes.get(lastPart);
    759  if (suffixes) {
    760    return Array.from(suffixes).some(s => asciiHost.endsWith(s));
    761  }
    762  return false;
    763 }
    764 
    765 /**
    766 * Checks the suffix of ``info.fixedURI`` against the Public Suffix List.
    767 * If the suffix is unknown due to a typo this will try to fix it up in-place,
    768 * by modifying the public suffix of ``info.fixedURI``.
    769 *
    770 * @param {URIFixupInfo} info about the uri to check.
    771 * @returns {object} result The lookup result.
    772 * @returns {string} result.suffix The public suffix if one can be identified.
    773 * @returns {boolean} result.hasUnknownSuffix True when the suffix is not in the
    774 *     Public Suffix List and it's not in knownSuffixes. False in the other cases.
    775 */
    776 function checkAndFixPublicSuffix(info) {
    777  let uri = info.fixedURI;
    778  let asciiHost = uri?.asciiHost;
    779 
    780  // If the original input ends in a "。" character (U+3002), we consider the
    781  // input a search query if there is no valid suffix.
    782  // While the "。" character is equivalent to a period in domains, it's more
    783  // commonly used to terminate search phrases. We're preserving the historical
    784  // behavior of the ascii period for now, as that may be more commonly expected
    785  // by technical users.
    786  if (
    787    !asciiHost ||
    788    !asciiHost.includes(".") ||
    789    (asciiHost.endsWith(".") && !info.originalInput.endsWith("。")) ||
    790    isDomainKnown(asciiHost)
    791  ) {
    792    return { suffix: "", hasUnknownSuffix: false };
    793  }
    794 
    795  // Quick bailouts for most common cases, according to Alexa Top 1 million.
    796  if (
    797    /^\w/.test(asciiHost) &&
    798    (asciiHost.endsWith(".com") ||
    799      asciiHost.endsWith(".net") ||
    800      asciiHost.endsWith(".org") ||
    801      asciiHost.endsWith(".ru") ||
    802      asciiHost.endsWith(".de"))
    803  ) {
    804    return {
    805      suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1),
    806      hasUnknownSuffix: false,
    807    };
    808  }
    809  try {
    810    let suffix = Services.eTLD.getKnownPublicSuffix(uri);
    811    if (suffix) {
    812      return { suffix, hasUnknownSuffix: false };
    813    }
    814  } catch (ex) {
    815    return { suffix: "", hasUnknownSuffix: false };
    816  }
    817  // Suffix is unknown, try to fix most common 3 chars TLDs typos.
    818  // .com is the most commonly mistyped tld, so it has more cases.
    819  let suffix = Services.eTLD.getPublicSuffix(uri);
    820  if (!suffix || lazy.numberRegex.test(suffix)) {
    821    return { suffix: "", hasUnknownSuffix: false };
    822  }
    823  for (let [typo, fixed] of [
    824    ["ocm", "com"],
    825    ["con", "com"],
    826    ["cmo", "com"],
    827    ["xom", "com"],
    828    ["vom", "com"],
    829    ["cpm", "com"],
    830    ["com'", "com"],
    831    ["ent", "net"],
    832    ["ner", "net"],
    833    ["nte", "net"],
    834    ["met", "net"],
    835    ["rog", "org"],
    836    ["ogr", "org"],
    837    ["prg", "org"],
    838    ["orh", "org"],
    839  ]) {
    840    if (suffix == typo) {
    841      let host = uri.host.substring(0, uri.host.length - typo.length) + fixed;
    842      let updatePreferredURI = info.preferredURI == info.fixedURI;
    843      info.fixedURI = uri.mutate().setHost(host).finalize();
    844      if (updatePreferredURI) {
    845        info.preferredURI = info.fixedURI;
    846      }
    847      return { suffix: fixed, hasUnknownSuffix: false };
    848    }
    849  }
    850  return { suffix: "", hasUnknownSuffix: true };
    851 }
    852 
    853 function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) {
    854  try {
    855    let keywordInfo = Services.uriFixup.keywordToURI(
    856      uriString,
    857      isPrivateContext
    858    );
    859    fixupInfo.keywordProviderName = keywordInfo.keywordProviderName;
    860    fixupInfo.keywordAsSent = keywordInfo.keywordAsSent;
    861    fixupInfo.preferredURI = keywordInfo.preferredURI;
    862    return true;
    863  } catch (ex) {}
    864  return false;
    865 }
    866 
    867 /**
    868 * This generates an alternate fixedURI, by adding a prefix and a suffix to
    869 * the fixedURI host, if and only if the protocol is http. It should _never_
    870 * modify URIs with other protocols.
    871 *
    872 * @param {URIFixupInfo} info an URIInfo object
    873 * @param {integer} fixupFlags the fixup flags
    874 * @returns {boolean} Whether an alternate uri was generated
    875 */
    876 function maybeSetAlternateFixedURI(info, fixupFlags) {
    877  let uri = info.fixedURI;
    878  if (
    879    !(fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI) ||
    880    // Code only works for http. Not for any other protocol including https!
    881    !uri.schemeIs("http") ||
    882    // Security - URLs with user / password info should NOT be fixed up
    883    uri.userPass ||
    884    // Don't fix up hosts with ports
    885    uri.port != -1
    886  ) {
    887    return false;
    888  }
    889 
    890  let oldHost = uri.host;
    891  // Don't create an alternate uri for localhost, because it would be confusing.
    892  // Ditto for 'http' and 'https' as these are frequently the result of typos, e.g.
    893  // 'https//foo' (note missing : ).
    894  if (oldHost == "localhost" || HTTPISH.has(oldHost)) {
    895    return false;
    896  }
    897 
    898  // Get the prefix and suffix to stick onto the new hostname. By default these
    899  // are www. & .com but they could be any other value, e.g. www. & .org
    900  let newHost = maybeAddPrefixAndSuffix(oldHost);
    901 
    902  if (newHost == oldHost) {
    903    return false;
    904  }
    905 
    906  return updateHostAndScheme(info, newHost);
    907 }
    908 
    909 /**
    910 * Try to fixup a file URI.
    911 *
    912 * @param {string} uriString The file URI to fix.
    913 * @returns {?nsIURI} a fixed uri if it has to add the file: protocol or null.
    914 */
    915 function fileURIFixup(uriString) {
    916  let attemptFixup = false;
    917  let path = uriString;
    918  if (AppConstants.platform == "win") {
    919    // Check for "\"" in the url-string, just a drive (e.g. C:),
    920    // or 'A:/...' where the "protocol" is also a single letter.
    921    attemptFixup =
    922      uriString.includes("\\") ||
    923      (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/"));
    924    if (uriString[1] == ":" && uriString[2] == "/") {
    925      path = uriString.replace(/\//g, "\\");
    926    }
    927  } else {
    928    // UNIX: Check if it starts with "/" or "~".
    929    attemptFixup = /^[~/]/.test(uriString);
    930    const originalHome = Services.env.get("BB_ORIGINAL_HOME");
    931    if (originalHome && (uriString === "~" || uriString.startsWith("~/"))) {
    932      path = originalHome + path.substring(1);
    933    }
    934  }
    935  if (attemptFixup) {
    936    try {
    937      // Test if this is a valid path by trying to create a local file
    938      // object. The URL of that is returned if successful.
    939      let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    940      file.initWithPath(path);
    941      return Services.io.newURI(
    942        lazy.fileProtocolHandler.getURLSpecFromActualFile(file)
    943      );
    944    } catch (ex) {
    945      // Not a file uri.
    946    }
    947  }
    948  return null;
    949 }
    950 
    951 /**
    952 * Tries to fixup a string to an nsIURI by adding the default protocol.
    953 *
    954 * Should fix things like:
    955 *    no-scheme.com
    956 *    ftp.no-scheme.com
    957 *    ftp4.no-scheme.com
    958 *    no-scheme.com/query?foo=http://www.foo.com
    959 *    user:pass@no-scheme.com
    960 *
    961 * @param {string} uriString The string to fixup.
    962 * @param {number} fixupFlags The fixup flags to use.
    963 * @returns {nsIURI} an nsIURI built adding the default protocol to the string,
    964 *          or null if fixing was not possible.
    965 */
    966 function fixupURIProtocol(uriString, fixupFlags) {
    967  // The longest URI scheme on the IANA list is 36 chars + 3 for ://
    968  let schemeChars = uriString.slice(0, 39);
    969 
    970  let schemePos = schemeChars.indexOf("://");
    971  if (schemePos == -1 || schemePos > schemeChars.search(/[:\/]/)) {
    972    uriString = "http://" + uriString;
    973  }
    974  try {
    975    return makeURIWithFixedLocalHosts(uriString, fixupFlags);
    976  } catch (ex) {
    977    // We generated an invalid uri.
    978  }
    979  return null;
    980 }
    981 
    982 /**
    983 * A thin wrapper around `newURI` that fixes up the host if it's
    984 * 0.0.0.0 or ::, which are no longer valid. Aims to facilitate
    985 * user typos and/or "broken" links output by commandline tools.
    986 *
    987 * @param {string} uriString The string to make into a URI.
    988 * @param {number} fixupFlags The fixup flags to use.
    989 * @throws NS_ERROR_MALFORMED_URI if the uri is invalid.
    990 */
    991 function makeURIWithFixedLocalHosts(uriString, fixupFlags) {
    992  let uri = Services.io.newURI(uriString);
    993 
    994  // We only want to fix up 0.0.0.0 if the URL came from the user, either
    995  // from the address bar or as a commandline argument (ie clicking links
    996  // in other applications, terminal, etc.). We can't use
    997  // FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP for this as that isn't normally allowed
    998  // for external links, and the other flags are sometimes used for
    999  // web-provided content. So we cheat and use the scheme typo flag.
   1000  if (fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS && HTTPISH.has(uri.scheme)) {
   1001    if (uri.host == "0.0.0.0") {
   1002      uri = uri.mutate().setHost("127.0.0.1").finalize();
   1003    } else if (uri.host == "::") {
   1004      uri = uri.mutate().setHost("[::1]").finalize();
   1005    }
   1006  }
   1007  return uri;
   1008 }
   1009 
   1010 /**
   1011 * Tries to fixup a string to a search url.
   1012 *
   1013 * @param {string} uriString the string to fixup.
   1014 * @param {URIFixupInfo} fixupInfo The fixup info object, modified in-place.
   1015 * @param {boolean} isPrivateContext Whether this happens in a private context.
   1016 * @param {nsIInputStream} postData optional POST data for the search
   1017 * @returns {boolean} Whether the keyword fixup was succesful.
   1018 */
   1019 function keywordURIFixup(uriString, fixupInfo, isPrivateContext) {
   1020  // Here is a few examples of strings that should be searched:
   1021  // "what is mozilla"
   1022  // "what is mozilla?"
   1023  // "docshell site:mozilla.org" - has a space in the origin part
   1024  // "?site:mozilla.org - anything that begins with a question mark
   1025  // "mozilla'.org" - Things that have a quote before the first dot/colon
   1026  // "mozilla/test" - unknown host
   1027  // ".mozilla", "mozilla." - starts or ends with a dot ()
   1028  // "user@nonQualifiedHost"
   1029 
   1030  // These other strings should not be searched, because they could be URIs:
   1031  // "www.blah.com" - Domain with a standard or known suffix
   1032  // "knowndomain" - known domain
   1033  // "nonQualifiedHost:8888?something" - has a port
   1034  // "user:pass@nonQualifiedHost"
   1035  // "blah.com."
   1036 
   1037  // We do keyword lookups if the input starts with a question mark.
   1038  if (uriString.startsWith("?")) {
   1039    return tryKeywordFixupForURIInfo(
   1040      fixupInfo.originalInput,
   1041      fixupInfo,
   1042      isPrivateContext
   1043    );
   1044  }
   1045 
   1046  // Check for IPs.
   1047  const userPassword = lazy.userPasswordRegex.exec(uriString);
   1048  const ipString = userPassword
   1049    ? uriString.replace(userPassword[2], "")
   1050    : uriString;
   1051  if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) {
   1052    return false;
   1053  }
   1054 
   1055  // Avoid keyword lookup if we can identify a host and it's known, or ends
   1056  // with a dot and has some path.
   1057  // Note that if dnsFirstForSingleWords is true isDomainKnown will always
   1058  // return true, so we can avoid checking dnsFirstForSingleWords after this.
   1059  let asciiHost = fixupInfo.fixedURI?.asciiHost;
   1060  if (
   1061    asciiHost &&
   1062    (isDomainKnown(asciiHost) ||
   1063      (asciiHost.endsWith(".") &&
   1064        asciiHost.indexOf(".") != asciiHost.length - 1))
   1065  ) {
   1066    return false;
   1067  }
   1068 
   1069  // Avoid keyword lookup if the url seems to have password.
   1070  if (fixupInfo.fixedURI?.password) {
   1071    return false;
   1072  }
   1073 
   1074  // Even if the host is unknown, avoid keyword lookup if the string has
   1075  // uri-like characteristics, unless it looks like "user@unknownHost".
   1076  // Note we already excluded passwords at this point.
   1077  if (
   1078    !isURILike(uriString, fixupInfo.fixedURI?.displayHost) ||
   1079    (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/")
   1080  ) {
   1081    return tryKeywordFixupForURIInfo(
   1082      fixupInfo.originalInput,
   1083      fixupInfo,
   1084      isPrivateContext
   1085    );
   1086  }
   1087 
   1088  return false;
   1089 }
   1090 
   1091 /**
   1092 * Mimics the logic in Services.io.extractScheme, but avoids crossing XPConnect.
   1093 * This also tries to fixup the scheme if it was clearly mistyped.
   1094 *
   1095 * @param {string} uriString the string to examine
   1096 * @param {integer} fixupFlags The original fixup flags
   1097 * @returns {object}
   1098 *          scheme: a typo fixed scheme or empty string if one could not be identified
   1099 *          fixedSchemeUriString: uri string with a typo fixed scheme
   1100 *          fixupChangedProtocol: true if the scheme is fixed up
   1101 */
   1102 function extractScheme(uriString, fixupFlags = FIXUP_FLAG_NONE) {
   1103  const matches = uriString.match(lazy.possibleProtocolRegex);
   1104  const hasColon = matches?.[2] === ":";
   1105  const hasSlash2 = matches?.[3] === "//";
   1106 
   1107  const isFixupSchemeTypos =
   1108    lazy.fixupSchemeTypos && fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS;
   1109 
   1110  if (
   1111    !matches ||
   1112    (!hasColon && !hasSlash2) ||
   1113    (!hasColon && !isFixupSchemeTypos)
   1114  ) {
   1115    return {
   1116      scheme: "",
   1117      fixedSchemeUriString: uriString,
   1118      fixupChangedProtocol: false,
   1119    };
   1120  }
   1121 
   1122  let scheme = matches[1].replace("\t", "").toLowerCase();
   1123  let fixedSchemeUriString = uriString;
   1124 
   1125  if (isFixupSchemeTypos && hasSlash2) {
   1126    // Fix up typos for string that user would have intented as protocol.
   1127    const afterProtocol = uriString.substring(matches[0].length);
   1128    fixedSchemeUriString = `${scheme}://${afterProtocol}`;
   1129  }
   1130 
   1131  let fixupChangedProtocol = false;
   1132 
   1133  if (isFixupSchemeTypos) {
   1134    // Fix up common scheme typos.
   1135    // TODO: Use levenshtein distance here?
   1136    fixupChangedProtocol = [
   1137      ["ttp", "http"],
   1138      ["htp", "http"],
   1139      ["ttps", "https"],
   1140      ["tps", "https"],
   1141      ["ps", "https"],
   1142      ["htps", "https"],
   1143      ["ile", "file"],
   1144      ["le", "file"],
   1145    ].some(([typo, fixed]) => {
   1146      if (scheme === typo) {
   1147        scheme = fixed;
   1148        fixedSchemeUriString =
   1149          scheme + fixedSchemeUriString.substring(typo.length);
   1150        return true;
   1151      }
   1152      return false;
   1153    });
   1154  }
   1155 
   1156  return {
   1157    scheme,
   1158    fixedSchemeUriString,
   1159    fixupChangedProtocol,
   1160  };
   1161 }
   1162 
   1163 /**
   1164 * View-source is a pseudo scheme. We're interested in fixing up the stuff
   1165 * after it. The easiest way to do that is to call this method again with
   1166 * the "view-source:" lopped off and then prepend it again afterwards.
   1167 *
   1168 * @param {string} uriString The original string to fixup
   1169 * @param {integer} fixupFlags The original fixup flags
   1170 * @param {nsIInputStream} postData Optional POST data for the search
   1171 * @returns {object} {preferredURI, postData} The fixed URI and relative postData
   1172 * @throws if it's not possible to fixup the url
   1173 */
   1174 function fixupViewSource(uriString, fixupFlags) {
   1175  // We disable keyword lookup and alternate URIs so that small typos don't
   1176  // cause us to look at very different domains.
   1177  let newFixupFlags = fixupFlags & ~FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   1178  let innerURIString = uriString.substring(12).trim();
   1179 
   1180  // Prevent recursion.
   1181  const { scheme: innerScheme } = extractScheme(innerURIString);
   1182  if (innerScheme == "view-source") {
   1183    throw new Components.Exception(
   1184      "Prevent view-source recursion",
   1185      Cr.NS_ERROR_FAILURE
   1186    );
   1187  }
   1188 
   1189  let info = Services.uriFixup.getFixupURIInfo(innerURIString, newFixupFlags);
   1190  if (!info.preferredURI) {
   1191    throw new Components.Exception(
   1192      "Couldn't build a valid uri",
   1193      Cr.NS_ERROR_MALFORMED_URI
   1194    );
   1195  }
   1196  return {
   1197    preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec),
   1198    postData: info.postData,
   1199  };
   1200 }
   1201 
   1202 /**
   1203 * Fixup the host of fixedURI if it contains consecutive dots.
   1204 *
   1205 * @param {URIFixupInfo} info an URIInfo object
   1206 */
   1207 function fixupConsecutiveDotsHost(fixupInfo) {
   1208  const uri = fixupInfo.fixedURI;
   1209 
   1210  try {
   1211    if (!uri?.host.includes("..")) {
   1212      return;
   1213    }
   1214  } catch (e) {
   1215    return;
   1216  }
   1217 
   1218  try {
   1219    const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri);
   1220 
   1221    fixupInfo.fixedURI = uri
   1222      .mutate()
   1223      .setHost(uri.host.replace(/\.+/g, "."))
   1224      .finalize();
   1225 
   1226    if (isPreferredEqualsToFixed) {
   1227      fixupInfo.preferredURI = fixupInfo.fixedURI;
   1228    }
   1229  } catch (e) {
   1230    if (e.result !== Cr.NS_ERROR_MALFORMED_URI) {
   1231      throw e;
   1232    }
   1233  }
   1234 }
   1235 
   1236 /**
   1237 * Return whether or not given string is uri like.
   1238 * This function returns true like following strings.
   1239 * - ":8080"
   1240 * - "localhost:8080" (if given host is "localhost")
   1241 * - "/foo?bar"
   1242 * - "/foo#bar"
   1243 *
   1244 * @param {string} uriString.
   1245 * @param {string} host.
   1246 * @param {boolean} true if uri like.
   1247 */
   1248 function isURILike(uriString, host) {
   1249  const indexOfSlash = uriString.indexOf("/");
   1250  if (
   1251    indexOfSlash >= 0 &&
   1252    (indexOfSlash < uriString.indexOf("?", indexOfSlash) ||
   1253      indexOfSlash < uriString.indexOf("#", indexOfSlash))
   1254  ) {
   1255    return true;
   1256  }
   1257 
   1258  if (uriString.startsWith(host)) {
   1259    uriString = uriString.substring(host.length);
   1260  }
   1261 
   1262  return lazy.portRegex.test(uriString);
   1263 }
   1264 
   1265 /**
   1266 * Add prefix and suffix to a hostname if both are missing.
   1267 *
   1268 * If the host does not start with the prefix, add the prefix to
   1269 * the hostname.
   1270 *
   1271 * By default the prefix and suffix are www. and .com but they could
   1272 * be any value e.g. www. and .org as they use the preferences
   1273 * "browser.fixup.alternate.prefix" and "browser.fixup.alternative.suffix"
   1274 *
   1275 * If no changes were made, it returns an empty string.
   1276 *
   1277 * @param {string} oldHost.
   1278 * @return {string} Fixed up hostname or an empty string.
   1279 */
   1280 function maybeAddPrefixAndSuffix(oldHost) {
   1281  let prefix = Services.prefs.getCharPref(
   1282    "browser.fixup.alternate.prefix",
   1283    "www."
   1284  );
   1285  let suffix = Services.prefs.getCharPref(
   1286    "browser.fixup.alternate.suffix",
   1287    ".com"
   1288  );
   1289  let newHost = "";
   1290  let numDots = (oldHost.match(/\./g) || []).length;
   1291  if (numDots == 0) {
   1292    newHost = prefix + oldHost + suffix;
   1293  } else if (numDots == 1) {
   1294    if (prefix && oldHost == prefix) {
   1295      newHost = oldHost + suffix;
   1296    } else if (suffix && !oldHost.startsWith(prefix)) {
   1297      newHost = prefix + oldHost;
   1298    }
   1299  }
   1300  return newHost ? newHost : oldHost;
   1301 }
   1302 
   1303 /**
   1304 * Given an instance of URIFixupInfo, update its fixedURI.
   1305 *
   1306 * First, change the protocol to the one stored in
   1307 * "browser.fixup.alternate.protocol".
   1308 *
   1309 * Then, try to update fixedURI's host to newHost.
   1310 *
   1311 * @param {URIFixupInfo} info.
   1312 * @param {string} newHost.
   1313 * @return {boolean}
   1314 *          True, if info was updated without any errors.
   1315 *          False, if NS_ERROR_MALFORMED_URI error.
   1316 * @throws If a non-NS_ERROR_MALFORMED_URI error occurs.
   1317 */
   1318 function updateHostAndScheme(info, newHost) {
   1319  let oldHost = info.fixedURI.host;
   1320  let oldScheme = info.fixedURI.scheme;
   1321  try {
   1322    info.fixedURI = info.fixedURI
   1323      .mutate()
   1324      .setScheme(lazy.alternateProtocol)
   1325      .setHost(newHost)
   1326      .finalize();
   1327  } catch (ex) {
   1328    if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
   1329      throw ex;
   1330    }
   1331    return false;
   1332  }
   1333  if (oldScheme != info.fixedURI.scheme) {
   1334    info.fixupChangedProtocol = true;
   1335  }
   1336  if (oldHost != info.fixedURI.host) {
   1337    info.fixupCreatedAlternateURI = true;
   1338  }
   1339  return true;
   1340 }