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 +};