summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm
blob: fedac17100b8aacdef3f517dbe08024cdbeeb19c (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
179
180
181
/* 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";

var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;

Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);

this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"];

const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;

/**
 * A probe implementing the measurements detailed at
 * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry
 *
 * This implementation uses only in-memory data.
 */
this.ThirdPartyCookieProbe = function() {
  /**
   * A set of third-party sites that have caused cookies to be
   * rejected. These sites are trimmed down to ETLD + 1
   * (i.e. "x.y.com" and "z.y.com" are both trimmed down to "y.com",
   * "x.y.co.uk" is trimmed down to "y.co.uk").
   *
   * Used to answer the following question: "For each third-party
   * site, how many other first parties embed them and result in
   * cookie traffic?" (see
   * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry#Breadth
   * )
   *
   * @type Map<string, RejectStats> A mapping from third-party site
   * to rejection statistics.
   */
  this._thirdPartyCookies = new Map();
  /**
   * Timestamp of the latest call to flush() in milliseconds since the Epoch.
   */
  this._latestFlush = Date.now();
};

this.ThirdPartyCookieProbe.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
  init: function() {
    Services.obs.addObserver(this, "profile-before-change", false);
    Services.obs.addObserver(this, "third-party-cookie-accepted", false);
    Services.obs.addObserver(this, "third-party-cookie-rejected", false);
  },
  dispose: function() {
    Services.obs.removeObserver(this, "profile-before-change");
    Services.obs.removeObserver(this, "third-party-cookie-accepted");
    Services.obs.removeObserver(this, "third-party-cookie-rejected");
  },
  /**
   * Observe either
   * - "profile-before-change" (no meaningful subject or data) - time to flush statistics and unregister; or
   * - "third-party-cookie-accepted"/"third-party-cookie-rejected" with
   *    subject: the nsIURI of the third-party that attempted to set the cookie;
   *    data: a string holding the uri of the page seen by the user.
   */
  observe: function(docURI, topic, referrer) {
    try {
      if (topic == "profile-before-change") {
        // A final flush, then unregister
        this.flush();
        this.dispose();
      }
      if (topic != "third-party-cookie-accepted"
          && topic != "third-party-cookie-rejected") {
        // Not a third-party cookie
        return;
      }
      // Add host to this._thirdPartyCookies
      // Note: nsCookieService passes "?" if the issuer is unknown.  Avoid
      //       normalizing in this case since its not a valid URI.
      let firstParty = (referrer === "?") ? referrer : normalizeHost(referrer);
      let thirdParty = normalizeHost(docURI.QueryInterface(Ci.nsIURI).host);
      let data = this._thirdPartyCookies.get(thirdParty);
      if (!data) {
        data = new RejectStats();
        this._thirdPartyCookies.set(thirdParty, data);
      }
      if (topic == "third-party-cookie-accepted") {
        data.addAccepted(firstParty);
      } else {
        data.addRejected(firstParty);
      }
    } catch (ex) {
      if (ex instanceof Ci.nsIXPCException) {
        if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
            ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
          return;
        }
      }
      // Other errors should not remain silent.
      Services.console.logStringMessage("ThirdPartyCookieProbe: Uncaught error " + ex + "\n" + ex.stack);
    }
  },

  /**
   * Clear internal data, fill up corresponding histograms.
   *
   * @param {number} aNow (optional, used for testing purposes only)
   * The current instant. Used to make tests time-independent.
   */
  flush: function(aNow = Date.now()) {
    let updays = (aNow - this._latestFlush) / MILLISECONDS_PER_DAY;
    if (updays <= 0) {
      // Unlikely, but regardless, don't risk division by zero
      // or weird stuff.
      return;
    }
    this._latestFlush = aNow;
    this._thirdPartyCookies.clear();
  }
};

/**
 * Data gathered on cookies that a third party site has attempted to set.
 *
 * Privacy note: the only data actually sent to the server is the size of
 * the sets.
 *
 * @constructor
 */
var RejectStats = function() {
  /**
   * The set of all sites for which we have accepted third-party cookies.
   */
  this._acceptedSites = new Set();
  /**
   * The set of all sites for which we have rejected third-party cookies.
   */
  this._rejectedSites = new Set();
  /**
   * Total number of attempts to set a third-party cookie that have
   * been accepted. Two accepted attempts on the same site will both
   * augment this count.
   */
  this._acceptedRequests = 0;
  /**
   * Total number of attempts to set a third-party cookie that have
   * been rejected. Two rejected attempts on the same site will both
   * augment this count.
   */
  this._rejectedRequests = 0;
};
RejectStats.prototype = {
  addAccepted: function(firstParty) {
    this._acceptedSites.add(firstParty);
    this._acceptedRequests++;
  },
  addRejected: function(firstParty) {
    this._rejectedSites.add(firstParty);
    this._rejectedRequests++;
  },
  get countAcceptedSites() {
    return this._acceptedSites.size;
  },
  get countRejectedSites() {
    return this._rejectedSites.size;
  },
  get countAcceptedRequests() {
    return this._acceptedRequests;
  },
  get countRejectedRequests() {
    return this._rejectedRequests;
  }
};

/**
 * Normalize a host to its eTLD + 1.
 */
function normalizeHost(host) {
  return Services.eTLD.getBaseDomainFromHost(host);
}