ThirdPartyCookieProbe.jsm 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. var Ci = Components.interfaces;
  6. var Cu = Components.utils;
  7. var Cr = Components.results;
  8. Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
  9. Cu.import("resource://gre/modules/Services.jsm", this);
  10. this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"];
  11. const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
  12. /**
  13. * A probe implementing the measurements detailed at
  14. * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry
  15. *
  16. * This implementation uses only in-memory data.
  17. */
  18. this.ThirdPartyCookieProbe = function() {
  19. /**
  20. * A set of third-party sites that have caused cookies to be
  21. * rejected. These sites are trimmed down to ETLD + 1
  22. * (i.e. "x.y.com" and "z.y.com" are both trimmed down to "y.com",
  23. * "x.y.co.uk" is trimmed down to "y.co.uk").
  24. *
  25. * Used to answer the following question: "For each third-party
  26. * site, how many other first parties embed them and result in
  27. * cookie traffic?" (see
  28. * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry#Breadth
  29. * )
  30. *
  31. * @type Map<string, RejectStats> A mapping from third-party site
  32. * to rejection statistics.
  33. */
  34. this._thirdPartyCookies = new Map();
  35. /**
  36. * Timestamp of the latest call to flush() in milliseconds since the Epoch.
  37. */
  38. this._latestFlush = Date.now();
  39. };
  40. this.ThirdPartyCookieProbe.prototype = {
  41. QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
  42. init: function() {
  43. Services.obs.addObserver(this, "profile-before-change", false);
  44. Services.obs.addObserver(this, "third-party-cookie-accepted", false);
  45. Services.obs.addObserver(this, "third-party-cookie-rejected", false);
  46. },
  47. dispose: function() {
  48. Services.obs.removeObserver(this, "profile-before-change");
  49. Services.obs.removeObserver(this, "third-party-cookie-accepted");
  50. Services.obs.removeObserver(this, "third-party-cookie-rejected");
  51. },
  52. /**
  53. * Observe either
  54. * - "profile-before-change" (no meaningful subject or data) - time to flush statistics and unregister; or
  55. * - "third-party-cookie-accepted"/"third-party-cookie-rejected" with
  56. * subject: the nsIURI of the third-party that attempted to set the cookie;
  57. * data: a string holding the uri of the page seen by the user.
  58. */
  59. observe: function(docURI, topic, referrer) {
  60. try {
  61. if (topic == "profile-before-change") {
  62. // A final flush, then unregister
  63. this.flush();
  64. this.dispose();
  65. }
  66. if (topic != "third-party-cookie-accepted"
  67. && topic != "third-party-cookie-rejected") {
  68. // Not a third-party cookie
  69. return;
  70. }
  71. // Add host to this._thirdPartyCookies
  72. // Note: nsCookieService passes "?" if the issuer is unknown. Avoid
  73. // normalizing in this case since its not a valid URI.
  74. let firstParty = (referrer === "?") ? referrer : normalizeHost(referrer);
  75. let thirdParty = normalizeHost(docURI.QueryInterface(Ci.nsIURI).host);
  76. let data = this._thirdPartyCookies.get(thirdParty);
  77. if (!data) {
  78. data = new RejectStats();
  79. this._thirdPartyCookies.set(thirdParty, data);
  80. }
  81. if (topic == "third-party-cookie-accepted") {
  82. data.addAccepted(firstParty);
  83. } else {
  84. data.addRejected(firstParty);
  85. }
  86. } catch (ex) {
  87. if (ex instanceof Ci.nsIXPCException) {
  88. if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
  89. ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
  90. return;
  91. }
  92. }
  93. // Other errors should not remain silent.
  94. Services.console.logStringMessage("ThirdPartyCookieProbe: Uncaught error " + ex + "\n" + ex.stack);
  95. }
  96. },
  97. /**
  98. * Clear internal data, fill up corresponding histograms.
  99. *
  100. * @param {number} aNow (optional, used for testing purposes only)
  101. * The current instant. Used to make tests time-independent.
  102. */
  103. flush: function(aNow = Date.now()) {
  104. let updays = (aNow - this._latestFlush) / MILLISECONDS_PER_DAY;
  105. if (updays <= 0) {
  106. // Unlikely, but regardless, don't risk division by zero
  107. // or weird stuff.
  108. return;
  109. }
  110. this._latestFlush = aNow;
  111. this._thirdPartyCookies.clear();
  112. }
  113. };
  114. /**
  115. * Data gathered on cookies that a third party site has attempted to set.
  116. *
  117. * Privacy note: the only data actually sent to the server is the size of
  118. * the sets.
  119. *
  120. * @constructor
  121. */
  122. var RejectStats = function() {
  123. /**
  124. * The set of all sites for which we have accepted third-party cookies.
  125. */
  126. this._acceptedSites = new Set();
  127. /**
  128. * The set of all sites for which we have rejected third-party cookies.
  129. */
  130. this._rejectedSites = new Set();
  131. /**
  132. * Total number of attempts to set a third-party cookie that have
  133. * been accepted. Two accepted attempts on the same site will both
  134. * augment this count.
  135. */
  136. this._acceptedRequests = 0;
  137. /**
  138. * Total number of attempts to set a third-party cookie that have
  139. * been rejected. Two rejected attempts on the same site will both
  140. * augment this count.
  141. */
  142. this._rejectedRequests = 0;
  143. };
  144. RejectStats.prototype = {
  145. addAccepted: function(firstParty) {
  146. this._acceptedSites.add(firstParty);
  147. this._acceptedRequests++;
  148. },
  149. addRejected: function(firstParty) {
  150. this._rejectedSites.add(firstParty);
  151. this._rejectedRequests++;
  152. },
  153. get countAcceptedSites() {
  154. return this._acceptedSites.size;
  155. },
  156. get countRejectedSites() {
  157. return this._rejectedSites.size;
  158. },
  159. get countAcceptedRequests() {
  160. return this._acceptedRequests;
  161. },
  162. get countRejectedRequests() {
  163. return this._rejectedRequests;
  164. }
  165. };
  166. /**
  167. * Normalize a host to its eTLD + 1.
  168. */
  169. function normalizeHost(host) {
  170. return Services.eTLD.getBaseDomainFromHost(host);
  171. }