tor-browser

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

api.js (11374B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* global ExtensionCommon, ExtensionAPI, Glean, Services, XPCOMUtils, ExtensionUtils */
      8 
      9 const { AddonManager } = ChromeUtils.importESModule(
     10  "resource://gre/modules/AddonManager.sys.mjs"
     11 );
     12 const { WebRequest } = ChromeUtils.importESModule(
     13  "resource://gre/modules/WebRequest.sys.mjs"
     14 );
     15 var { ExtensionParent } = ChromeUtils.importESModule(
     16  "resource://gre/modules/ExtensionParent.sys.mjs"
     17 );
     18 const lazy = {};
     19 
     20 ChromeUtils.defineESModuleGetters(lazy, {
     21  AddonSearchEngine:
     22    "moz-src:///toolkit/components/search/AddonSearchEngine.sys.mjs",
     23  ConfigSearchEngine:
     24    "moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
     25 });
     26 
     27 // eslint-disable-next-line mozilla/reject-importGlobalProperties
     28 XPCOMUtils.defineLazyGlobalGetters(this, ["ChannelWrapper", "URLSearchParams"]);
     29 
     30 const SEARCH_TOPIC_ENGINE_MODIFIED = "browser-search-engine-modified";
     31 
     32 this.addonsSearchDetection = class extends ExtensionAPI {
     33  getAPI(context) {
     34    const { extension } = context;
     35 
     36    // We want to temporarily store the first monitored URLs that have been
     37    // redirected, indexed by request IDs, so that the background script can
     38    // find the add-on IDs to report.
     39    this.firstMatchedUrls = {};
     40 
     41    return {
     42      addonsSearchDetection: {
     43        // `getEngines()` returns an array of engines, each with a baseUrl.
     44        // AppProvided engines also include a partner paramName,
     45        // while Addon provided engines include the addonId.
     46        //
     47        // Note: We don't return a simple list of URL patterns because the
     48        // background script might want to lookup add-on IDs for a given URL in
     49        // the case of server-side redirects.
     50        async getEngines() {
     51          const results = [];
     52 
     53          try {
     54            // Delaying accessing Services.search if we didn't get to first paint yet
     55            // to avoid triggering search internals from loading too soon during the
     56            // application startup.
     57            if (
     58              !Cu.isESModuleLoaded(
     59                "resource://gre/modules/SearchService.sys.mjs"
     60              )
     61            ) {
     62              await ExtensionParent.browserPaintedPromise;
     63            }
     64            // Return earlier if the extension or the application is shutting down.
     65            if (extension.hasShutdown || Services.startup.shuttingDown) {
     66              return results;
     67            }
     68            await Services.search.promiseInitialized;
     69            const engines = await Services.search.getEngines();
     70 
     71            for (let engine of engines) {
     72              if (
     73                !(engine instanceof lazy.AddonSearchEngine) &&
     74                !(engine instanceof lazy.ConfigSearchEngine)
     75              ) {
     76                continue;
     77              }
     78 
     79              // The search term isn't used, but avoids a warning of an empty
     80              // term.
     81              let submission = engine.getSubmission("searchTerm");
     82              if (submission) {
     83                const uri = submission.uri;
     84                const baseUrl = uri.prePath + uri.filePath;
     85 
     86                // We don't store ids for application provided search engines
     87                // because we don't need to report them. However, we do ensure
     88                // the pattern is recorded (above), so that we check for
     89                // redirects against those.
     90                const addonId = engine.wrappedJSObject._extensionID;
     91 
     92                let paramName;
     93                for (let [key, value] of new URLSearchParams(uri.query)) {
     94                  if (value && value === engine.partnerCode) {
     95                    paramName = key;
     96                  }
     97                }
     98 
     99                results.push({ baseUrl, addonId, paramName });
    100              }
    101            }
    102          } catch (err) {
    103            console.error(err);
    104          }
    105 
    106          return results;
    107        },
    108 
    109        // `getAddonVersion()` returns the add-on version if it exists.
    110        async getAddonVersion(addonId) {
    111          const addon = await AddonManager.getAddonByID(addonId);
    112 
    113          return addon && addon.version;
    114        },
    115 
    116        // `getPublicSuffix()` returns the public suffix/Effective TLD Service
    117        // of the given URL. See: `nsIEffectiveTLDService` interface in tree.
    118        async getPublicSuffix(url) {
    119          try {
    120            return Services.eTLD.getBaseDomain(Services.io.newURI(url));
    121          } catch (err) {
    122            console.error(err);
    123            return null;
    124          }
    125        },
    126 
    127        reportSameSiteRedirect(extra) {
    128          Glean.addonsSearchDetection.sameSiteRedirect.record(extra);
    129        },
    130 
    131        reportETLDChangeOther(extra) {
    132          Glean.addonsSearchDetection.etldChangeOther.record(extra);
    133        },
    134 
    135        reportETLDChangeWebrequest(extra) {
    136          Glean.addonsSearchDetection.etldChangeWebrequest.record(extra);
    137        },
    138 
    139        // `onSearchEngineModified` is an event that occurs when the list of
    140        // search engines has changed, e.g., a new engine has been added or an
    141        // engine has been removed.
    142        //
    143        // See: https://searchfox.org/mozilla-central/rev/cb44fc4f7bb84f2a18fedba64c8563770df13e34/toolkit/components/search/SearchUtils.sys.mjs#185-193
    144        onSearchEngineModified: new ExtensionCommon.EventManager({
    145          context,
    146          name: "addonsSearchDetection.onSearchEngineModified",
    147          register: fire => {
    148            const onSearchEngineModifiedObserver = (
    149              aSubject,
    150              aTopic,
    151              aData
    152            ) => {
    153              if (
    154                aTopic !== SEARCH_TOPIC_ENGINE_MODIFIED ||
    155                // We are only interested in these modified types.
    156                !["engine-added", "engine-removed", "engine-changed"].includes(
    157                  aData
    158                )
    159              ) {
    160                return;
    161              }
    162 
    163              fire.async();
    164            };
    165 
    166            Services.obs.addObserver(
    167              onSearchEngineModifiedObserver,
    168              SEARCH_TOPIC_ENGINE_MODIFIED
    169            );
    170 
    171            return () => {
    172              Services.obs.removeObserver(
    173                onSearchEngineModifiedObserver,
    174                SEARCH_TOPIC_ENGINE_MODIFIED
    175              );
    176            };
    177          },
    178        }).api(),
    179 
    180        // `onRedirected` is an event fired after a request has stopped and
    181        // this request has been redirected once or more. The registered
    182        // listeners will received the following properties:
    183        //
    184        //   - `addonId`: the add-on ID that redirected the request, if any.
    185        //   - `firstUrl`: the first monitored URL of the request that has
    186        //      been redirected.
    187        //   - `lastUrl`: the last URL loaded for the request, after one or
    188        //      more redirects.
    189        onRedirected: new ExtensionCommon.EventManager({
    190          context,
    191          name: "addonsSearchDetection.onRedirected",
    192          register: (fire, filter) => {
    193            const stopListener = event => {
    194              if (event.type != "stop") {
    195                return;
    196              }
    197 
    198              const wrapper = event.currentTarget;
    199              const { channel, id: requestId } = wrapper;
    200 
    201              // When we detected a redirect, we read the request property,
    202              // hoping to find an add-on ID corresponding to the add-on that
    203              // initiated the redirect. It might not return anything when the
    204              // redirect is a search server-side redirect but it can also be
    205              // caused by an error.
    206              let addonId;
    207              try {
    208                addonId = channel
    209                  ?.QueryInterface(Ci.nsIPropertyBag)
    210                  ?.getProperty("redirectedByExtension");
    211              } catch (err) {
    212                console.error(err);
    213              }
    214 
    215              const firstUrl = this.firstMatchedUrls[requestId];
    216              // We don't need this entry anymore.
    217              delete this.firstMatchedUrls[requestId];
    218 
    219              const lastUrl = wrapper.finalURL;
    220 
    221              if (!firstUrl || !lastUrl) {
    222                // Something went wrong but there is nothing we can do at this
    223                // point.
    224                return;
    225              }
    226 
    227              fire.sync({ addonId, firstUrl, lastUrl });
    228            };
    229 
    230            const remoteTab = context.xulBrowser.frameLoader.remoteTab;
    231 
    232            const listener = ({ requestId, url, originUrl }) => {
    233              // We exclude requests not originating from the location bar,
    234              // bookmarks and other "system-ish" requests.
    235              if (originUrl !== undefined) {
    236                return;
    237              }
    238 
    239              // Keep the first monitored URL that was redirected for the
    240              // request until the request has stopped.
    241              if (!this.firstMatchedUrls[requestId]) {
    242                this.firstMatchedUrls[requestId] = url;
    243 
    244                const wrapper = ChannelWrapper.getRegisteredChannel(
    245                  requestId,
    246                  context.extension.policy,
    247                  remoteTab
    248                );
    249 
    250                wrapper.addEventListener("stop", stopListener);
    251              }
    252            };
    253 
    254            const ensureRegisterChannel = data => {
    255              // onRedirected depends on ChannelWrapper.getRegisteredChannel,
    256              // which in turn depends on registerTraceableChannel to have been
    257              // called. When a blocking webRequest listener is present, the
    258              // parent/ext-webRequest.js implementation already calls that.
    259              //
    260              // A downside to a blocking webRequest listener is that it delays
    261              // the network request until a roundtrip to the listener in the
    262              // extension process has happened. Since we don't need to handle
    263              // the onBeforeRequest event, avoid the overhead by handling the
    264              // event and registration here, in the parent process.
    265              data.registerTraceableChannel(extension.policy, remoteTab);
    266            };
    267 
    268            const parsedFilter = {
    269              types: ["main_frame"],
    270              urls: ExtensionUtils.parseMatchPatterns(filter.urls),
    271            };
    272 
    273            WebRequest.onBeforeRequest.addListener(
    274              ensureRegisterChannel,
    275              parsedFilter,
    276              // blocking is needed to unlock data.registerTraceableChannel.
    277              ["blocking"],
    278              {
    279                addonId: extension.id,
    280                policy: extension.policy,
    281                blockingAllowed: true,
    282              }
    283            );
    284 
    285            WebRequest.onBeforeRedirect.addListener(
    286              listener,
    287              parsedFilter,
    288              // info
    289              [],
    290              // listener details
    291              {
    292                addonId: extension.id,
    293                policy: extension.policy,
    294                blockingAllowed: false,
    295              }
    296            );
    297 
    298            return () => {
    299              WebRequest.onBeforeRequest.removeListener(ensureRegisterChannel);
    300              WebRequest.onBeforeRedirect.removeListener(listener);
    301            };
    302          },
    303        }).api(),
    304      },
    305    };
    306  }
    307 };