toolkit/components/telemetry/ThirdPartyCookieProbe.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,191 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +let Ci = Components.interfaces;
    1.11 +let Cu = Components.utils;
    1.12 +let Cr = Components.results;
    1.13 +
    1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.15 +Cu.import("resource://gre/modules/Services.jsm", this);
    1.16 +
    1.17 +this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"];
    1.18 +
    1.19 +const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
    1.20 +
    1.21 +/**
    1.22 + * A probe implementing the measurements detailed at
    1.23 + * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry
    1.24 + *
    1.25 + * This implementation uses only in-memory data.
    1.26 + */
    1.27 +this.ThirdPartyCookieProbe = function() {
    1.28 +  /**
    1.29 +   * A set of third-party sites that have caused cookies to be
    1.30 +   * rejected. These sites are trimmed down to ETLD + 1
    1.31 +   * (i.e. "x.y.com" and "z.y.com" are both trimmed down to "y.com",
    1.32 +   * "x.y.co.uk" is trimmed down to "y.co.uk").
    1.33 +   *
    1.34 +   * Used to answer the following question: "For each third-party
    1.35 +   * site, how many other first parties embed them and result in
    1.36 +   * cookie traffic?" (see
    1.37 +   * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry#Breadth
    1.38 +   * )
    1.39 +   *
    1.40 +   * @type Map<string, RejectStats> A mapping from third-party site
    1.41 +   * to rejection statistics.
    1.42 +   */
    1.43 +  this._thirdPartyCookies = new Map();
    1.44 +  /**
    1.45 +   * Timestamp of the latest call to flush() in milliseconds since the Epoch.
    1.46 +   */
    1.47 +  this._latestFlush = Date.now();
    1.48 +};
    1.49 +
    1.50 +this.ThirdPartyCookieProbe.prototype = {
    1.51 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
    1.52 +  init: function() {
    1.53 +    Services.obs.addObserver(this, "profile-before-change", false);
    1.54 +    Services.obs.addObserver(this, "third-party-cookie-accepted", false);
    1.55 +    Services.obs.addObserver(this, "third-party-cookie-rejected", false);
    1.56 +  },
    1.57 +  dispose: function() {
    1.58 +    Services.obs.removeObserver(this, "profile-before-change");
    1.59 +    Services.obs.removeObserver(this, "third-party-cookie-accepted");
    1.60 +    Services.obs.removeObserver(this, "third-party-cookie-rejected");
    1.61 +  },
    1.62 +  /**
    1.63 +   * Observe either
    1.64 +   * - "profile-before-change" (no meaningful subject or data) - time to flush statistics and unregister; or
    1.65 +   * - "third-party-cookie-accepted"/"third-party-cookie-rejected" with
    1.66 +   *    subject: the nsIURI of the third-party that attempted to set the cookie;
    1.67 +   *    data: a string holding the uri of the page seen by the user.
    1.68 +   */
    1.69 +  observe: function(docURI, topic, referrer) {
    1.70 +    try {
    1.71 +      if (topic == "profile-before-change") {
    1.72 +        // A final flush, then unregister
    1.73 +        this.flush();
    1.74 +        this.dispose();
    1.75 +      }
    1.76 +      if (topic != "third-party-cookie-accepted"
    1.77 +          && topic != "third-party-cookie-rejected") {
    1.78 +        // Not a third-party cookie
    1.79 +        return;
    1.80 +      }
    1.81 +      // Add host to this._thirdPartyCookies
    1.82 +      // Note: nsCookieService passes "?" if the issuer is unknown.  Avoid
    1.83 +      //       normalizing in this case since its not a valid URI.
    1.84 +      let firstParty = (referrer === "?") ? referrer : normalizeHost(referrer);
    1.85 +      let thirdParty = normalizeHost(docURI.QueryInterface(Ci.nsIURI).host);
    1.86 +      let data = this._thirdPartyCookies.get(thirdParty);
    1.87 +      if (!data) {
    1.88 +        data = new RejectStats();
    1.89 +        this._thirdPartyCookies.set(thirdParty, data);
    1.90 +      }
    1.91 +      if (topic == "third-party-cookie-accepted") {
    1.92 +        data.addAccepted(firstParty);
    1.93 +      } else {
    1.94 +        data.addRejected(firstParty);
    1.95 +      }
    1.96 +    } catch (ex) {
    1.97 +      if (ex instanceof Ci.nsIXPCException) {
    1.98 +        if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
    1.99 +            ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
   1.100 +          return;
   1.101 +        }
   1.102 +      }
   1.103 +      // Other errors should not remain silent.
   1.104 +      Services.console.logStringMessage("ThirdPartyCookieProbe: Uncaught error " + ex + "\n" + ex.stack);
   1.105 +    }
   1.106 +  },
   1.107 +
   1.108 +  /**
   1.109 +   * Clear internal data, fill up corresponding histograms.
   1.110 +   *
   1.111 +   * @param {number} aNow (optional, used for testing purposes only)
   1.112 +   * The current instant. Used to make tests time-independent.
   1.113 +   */
   1.114 +  flush: function(aNow = Date.now()) {
   1.115 +    let updays = (aNow - this._latestFlush) / MILLISECONDS_PER_DAY;
   1.116 +    if (updays <= 0) {
   1.117 +      // Unlikely, but regardless, don't risk division by zero
   1.118 +      // or weird stuff.
   1.119 +      return;
   1.120 +    }
   1.121 +    this._latestFlush = aNow;
   1.122 +    let acceptedSites = Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_SITES_ACCEPTED");
   1.123 +    let rejectedSites = Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_SITES_BLOCKED");
   1.124 +    let acceptedRequests = Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_ATTEMPTS_ACCEPTED");
   1.125 +    let rejectedRequests = Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_ATTEMPTS_BLOCKED");
   1.126 +    for (let [k, data] of this._thirdPartyCookies) {
   1.127 +      acceptedSites.add(data.countAcceptedSites / updays);
   1.128 +      rejectedSites.add(data.countRejectedSites / updays);
   1.129 +      acceptedRequests.add(data.countAcceptedRequests / updays);
   1.130 +      rejectedRequests.add(data.countRejectedRequests / updays);
   1.131 +    }
   1.132 +    this._thirdPartyCookies.clear();
   1.133 +  }
   1.134 +};
   1.135 +
   1.136 +/**
   1.137 + * Data gathered on cookies that a third party site has attempted to set.
   1.138 + *
   1.139 + * Privacy note: the only data actually sent to the server is the size of
   1.140 + * the sets.
   1.141 + *
   1.142 + * @constructor
   1.143 + */
   1.144 +let RejectStats = function() {
   1.145 +  /**
   1.146 +   * The set of all sites for which we have accepted third-party cookies.
   1.147 +   */
   1.148 +  this._acceptedSites = new Set();
   1.149 +  /**
   1.150 +   * The set of all sites for which we have rejected third-party cookies.
   1.151 +   */
   1.152 +  this._rejectedSites = new Set();
   1.153 +  /**
   1.154 +   * Total number of attempts to set a third-party cookie that have
   1.155 +   * been accepted. Two accepted attempts on the same site will both
   1.156 +   * augment this count.
   1.157 +   */
   1.158 +  this._acceptedRequests = 0;
   1.159 +  /**
   1.160 +   * Total number of attempts to set a third-party cookie that have
   1.161 +   * been rejected. Two rejected attempts on the same site will both
   1.162 +   * augment this count.
   1.163 +   */
   1.164 +  this._rejectedRequests = 0;
   1.165 +};
   1.166 +RejectStats.prototype = {
   1.167 +  addAccepted: function(firstParty) {
   1.168 +    this._acceptedSites.add(firstParty);
   1.169 +    this._acceptedRequests++;
   1.170 +  },
   1.171 +  addRejected: function(firstParty) {
   1.172 +    this._rejectedSites.add(firstParty);
   1.173 +    this._rejectedRequests++;
   1.174 +  },
   1.175 +  get countAcceptedSites() {
   1.176 +    return this._acceptedSites.size;
   1.177 +  },
   1.178 +  get countRejectedSites() {
   1.179 +    return this._rejectedSites.size;
   1.180 +  },
   1.181 +  get countAcceptedRequests() {
   1.182 +    return this._acceptedRequests;
   1.183 +  },
   1.184 +  get countRejectedRequests() {
   1.185 +    return this._rejectedRequests;
   1.186 +  }
   1.187 +};
   1.188 +
   1.189 +/**
   1.190 + * Normalize a host to its eTLD + 1.
   1.191 + */
   1.192 +function normalizeHost(host) {
   1.193 +  return Services.eTLD.getBaseDomainFromHost(host);
   1.194 +};

mercurial