summaryrefslogtreecommitdiffstats
path: root/browser/extensions/search-detection/extension/background.js
blob: 043bb0243f83fb81b169de442b70086b2e6c69f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* global browser */

const TELEMETRY_CATEGORY = "addonsSearchDetection";
// methods
const TELEMETRY_METHOD_ETLD_CHANGE = "etld_change";
// objects
const TELEMETRY_OBJECT_WEBREQUEST = "webrequest";
const TELEMETRY_OBJECT_OTHER = "other";
// values
const TELEMETRY_VALUE_EXTENSION = "extension";
const TELEMETRY_VALUE_SERVER = "server";

class AddonsSearchDetection {
  constructor() {
    // The key is an URL pattern to monitor and its corresponding value is a
    // list of add-on IDs.
    this.matchPatterns = {};

    browser.telemetry.registerEvents(TELEMETRY_CATEGORY, {
      [TELEMETRY_METHOD_ETLD_CHANGE]: {
        methods: [TELEMETRY_METHOD_ETLD_CHANGE],
        objects: [TELEMETRY_OBJECT_WEBREQUEST, TELEMETRY_OBJECT_OTHER],
        extra_keys: ["addonId", "addonVersion", "from", "to"],
        record_on_release: true,
      },
    });

    this.onRedirectedListener = this.onRedirectedListener.bind(this);
  }

  async getMatchPatterns() {
    try {
      this.matchPatterns =
        await browser.addonsSearchDetection.getMatchPatterns();
    } catch (err) {
      console.error(`failed to retrieve the list of URL patterns: ${err}`);
      this.matchPatterns = {};
    }

    return this.matchPatterns;
  }

  // When the search service changes the set of engines that are enabled, we
  // update our pattern matching in the webrequest listeners (go to the bottom
  // of this file for the search service events we listen to).
  async monitor() {
    // If there is already a listener, remove it so that we can re-add one
    // after. This is because we're using the same listener with different URL
    // patterns (when the list of search engines changes).
    if (
      browser.addonsSearchDetection.onRedirected.hasListener(
        this.onRedirectedListener
      )
    ) {
      browser.addonsSearchDetection.onRedirected.removeListener(
        this.onRedirectedListener
      );
    }
    // If there is already a listener, remove it so that we can re-add one
    // after. This is because we're using the same listener with different URL
    // patterns (when the list of search engines changes).
    if (browser.webRequest.onBeforeRequest.hasListener(this.noOpListener)) {
      browser.webRequest.onBeforeRequest.removeListener(this.noOpListener);
    }

    // Retrieve the list of URL patterns to monitor with our listener.
    //
    // Note: search suggestions are system principal requests, so webRequest
    // cannot intercept them.
    const matchPatterns = await this.getMatchPatterns();
    const patterns = Object.keys(matchPatterns);

    if (patterns.length === 0) {
      return;
    }

    browser.webRequest.onBeforeRequest.addListener(
      this.noOpListener,
      { types: ["main_frame"], urls: patterns },
      ["blocking"]
    );

    browser.addonsSearchDetection.onRedirected.addListener(
      this.onRedirectedListener,
      { urls: patterns }
    );
  }

  // This listener is required to force the registration of traceable channels.
  noOpListener() {
    // Do nothing.
  }

  async onRedirectedListener({ addonId, firstUrl, lastUrl }) {
    // When we do not have an add-on ID (in the request property bag), we
    // likely detected a search server-side redirect.
    const maybeServerSideRedirect = !addonId;

    let addonIds = [];
    // Search server-side redirects are possible because an extension has
    // registered a search engine, which is why we can (hopefully) retrieve the
    // add-on ID.
    if (maybeServerSideRedirect) {
      addonIds = this.getAddonIdsForUrl(firstUrl);
    } else if (addonId) {
      addonIds = [addonId];
    }

    if (addonIds.length === 0) {
      // No add-on ID means there is nothing we can report.
      return;
    }

    // This is the monitored URL that was first redirected.
    const from = await browser.addonsSearchDetection.getPublicSuffix(firstUrl);
    // This is the final URL after redirect(s).
    const to = await browser.addonsSearchDetection.getPublicSuffix(lastUrl);

    if (from === to) {
      // We do not want to report redirects to same public suffixes. However,
      // we will report redirects from public suffixes belonging to a same
      // entity (.e.g., `example.com` -> `example.fr`).
      //
      // Known limitation: if a redirect chain starts and ends with the same
      // public suffix, we won't report any event, even if the chain contains
      // different public suffixes in between.
      return;
    }

    const telemetryObject = maybeServerSideRedirect
      ? TELEMETRY_OBJECT_OTHER
      : TELEMETRY_OBJECT_WEBREQUEST;
    const telemetryValue = maybeServerSideRedirect
      ? TELEMETRY_VALUE_SERVER
      : TELEMETRY_VALUE_EXTENSION;

    for (const id of addonIds) {
      const addonVersion = await browser.addonsSearchDetection.getAddonVersion(
        id
      );
      const extra = { addonId: id, addonVersion, from, to };

      browser.telemetry.recordEvent(
        TELEMETRY_CATEGORY,
        TELEMETRY_METHOD_ETLD_CHANGE,
        telemetryObject,
        telemetryValue,
        extra
      );
    }
  }

  getAddonIdsForUrl(url) {
    for (const pattern of Object.keys(this.matchPatterns)) {
      // `getMatchPatterns()` returns the prefix plus "*".
      const urlPrefix = pattern.slice(0, -1);

      if (url.startsWith(urlPrefix)) {
        return this.matchPatterns[pattern];
      }
    }

    return [];
  }
}

const exp = new AddonsSearchDetection();
exp.monitor();

browser.addonsSearchDetection.onSearchEngineModified.addListener(async () => {
  await exp.monitor();
});