Bug 1508310 - Implement Report-to header support - part 12 - Cleanup out-of-date endpoints, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Sat, 01 Dec 2018 21:26:10 +0100
changeset 505586 77156cbacc33c72a63fdf12a0c9229563b25072e
parent 505585 ec7421212c2218eb208eb08457fb660a81a6572d
child 505587 870d4a920aed8fe2251be8f81d824384d71ce38e
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1508310
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1508310 - Implement Report-to header support - part 12 - Cleanup out-of-date endpoints, r=smaug
dom/reporting/ReportingHeader.cpp
dom/reporting/ReportingHeader.h
dom/reporting/tests/browser_cleanup.js
dom/reporting/tests/delivering.sjs
modules/libpref/init/StaticPrefList.h
--- a/dom/reporting/ReportingHeader.cpp
+++ b/dom/reporting/ReportingHeader.cpp
@@ -63,16 +63,21 @@ StaticRefPtr<ReportingHeader> gReporting
 
   if (!gReporting) {
     return;
   }
 
   RefPtr<ReportingHeader> service = gReporting;
   gReporting = nullptr;
 
+  if (service->mCleanupTimer) {
+    service->mCleanupTimer->Cancel();
+    service->mCleanupTimer = nullptr;
+  }
+
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (NS_WARN_IF(!obs)) {
     return;
   }
 
   obs->RemoveObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC);
   obs->RemoveObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
   obs->RemoveObserver(service, "browser:purge-domain-data");
@@ -173,20 +178,24 @@ void ReportingHeader::ReportingFromChann
 
   nsAutoCString origin;
   rv = principal->GetOrigin(origin);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   UniquePtr<Client> client = ParseHeader(aChannel, uri, headerValue);
-  if (client) {
-    // Here we override the previous data.
-    mOrigins.Put(origin, client.release());
+  if (!client) {
+    return;
   }
+
+  // Here we override the previous data.
+  mOrigins.Put(origin, client.release());
+
+  MaybeCreateCleanupTimer();
 }
 
 /* static */ UniquePtr<ReportingHeader::Client> ReportingHeader::ParseHeader(
     nsIHttpChannel* aChannel, nsIURI* aURI, const nsACString& aHeaderValue) {
   MOZ_ASSERT(aURI);
   // aChannel can be null in gtest
 
   AutoJSAPI jsapi;
@@ -608,16 +617,17 @@ bool ReportingHeader::IsSecureURI(nsIURI
       }
 
       break;
     }
   }
 
   if (client->mGroups.IsEmpty()) {
     gReporting->mOrigins.Remove(origin);
+    gReporting->MaybeCancelCleanupTimer();
   }
 }
 
 void ReportingHeader::RemoveOriginsFromHost(const nsAString& aHost) {
   nsCOMPtr<nsIEffectiveTLDService> tldService =
       do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
   if (NS_WARN_IF(!tldService)) {
     return;
@@ -629,46 +639,119 @@ void ReportingHeader::RemoveOriginsFromH
     bool hasRootDomain = false;
     nsresult rv = tldService->HasRootDomain(iter.Key(), host, &hasRootDomain);
     if (NS_WARN_IF(NS_FAILED(rv)) || !hasRootDomain) {
       continue;
     }
 
     iter.Remove();
   }
+
+  MaybeCancelCleanupTimer();
 }
 
 void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
     const OriginAttributesPattern& aPattern) {
   for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
     nsAutoCString suffix;
     OriginAttributes attr;
     if (NS_WARN_IF(!attr.PopulateFromOrigin(iter.Key(), suffix))) {
       continue;
     }
 
     if (aPattern.Matches(attr)) {
       iter.Remove();
     }
   }
+
+  MaybeCancelCleanupTimer();
+}
+
+void ReportingHeader::RemoveOrigins() {
+  mOrigins.Clear();
+  MaybeCancelCleanupTimer();
 }
 
-void ReportingHeader::RemoveOrigins() { mOrigins.Clear(); }
+void ReportingHeader::RemoveOriginsForTTL() {
+  TimeStamp now = TimeStamp::Now();
+
+  for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
+    Client* client = iter.UserData();
+
+    // Scope of the iterator.
+    {
+      nsTObserverArray<Group>::BackwardIterator groupIter(client->mGroups);
+      while (groupIter.HasMore()) {
+        const Group& group = groupIter.GetNext();
+        TimeDuration diff = now - group.mCreationTime;
+        if (diff.ToSeconds() > group.mTTL) {
+          groupIter.Remove();
+          return;
+        }
+      }
+    }
+
+    if (client->mGroups.IsEmpty()) {
+      iter.Remove();
+    }
+  }
+}
 
 /* static */ bool ReportingHeader::HasReportingHeaderForOrigin(
     const nsACString& aOrigin) {
   if (!gReporting) {
     return false;
   }
 
   return gReporting->mOrigins.Contains(aOrigin);
 }
 
+NS_IMETHODIMP
+ReportingHeader::Notify(nsITimer* aTimer) {
+  mCleanupTimer = nullptr;
+
+  RemoveOriginsForTTL();
+  MaybeCreateCleanupTimer();
+
+  return NS_OK;
+}
+
+void ReportingHeader::MaybeCreateCleanupTimer() {
+  if (mCleanupTimer) {
+    return;
+  }
+
+  if (mOrigins.Count() == 0) {
+    return;
+  }
+
+  uint32_t timeout = StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
+  nsresult rv =
+      NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer), this, timeout,
+                              nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                              SystemGroup::EventTargetFor(TaskCategory::Other));
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+}
+
+void ReportingHeader::MaybeCancelCleanupTimer() {
+  if (!mCleanupTimer) {
+    return;
+  }
+
+  if (mOrigins.Count() != 0) {
+    return;
+  }
+
+  mCleanupTimer->Cancel();
+  mCleanupTimer = nullptr;
+}
+
 NS_INTERFACE_MAP_BEGIN(ReportingHeader)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
+  NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(ReportingHeader)
 NS_IMPL_RELEASE(ReportingHeader)
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/reporting/ReportingHeader.h
+++ b/dom/reporting/ReportingHeader.h
@@ -5,36 +5,38 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_ReportingHeader_h
 #define mozilla_dom_ReportingHeader_h
 
 #include "mozilla/TimeStamp.h"
 #include "nsClassHashtable.h"
 #include "nsIObserver.h"
+#include "nsITimer.h"
 #include "nsTObserverArray.h"
 
 class nsIHttpChannel;
 class nsIPrincipal;
 class nsIURI;
 
 namespace mozilla {
 
 class OriginAttributesPattern;
 
 namespace ipc {
 class PrincipalInfo;
 }
 
 namespace dom {
 
-class ReportingHeader final : public nsIObserver {
+class ReportingHeader final : public nsIObserver, public nsITimerCallback {
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
+  NS_DECL_NSITIMERCALLBACK
 
   static void Initialize();
 
   // Exposed structs for gtests
 
   struct Endpoint {
     nsCOMPtr<nsIURI> mUrl;
     uint32_t mPriority;
@@ -84,16 +86,22 @@ class ReportingHeader final : public nsI
 
   void RemoveOriginsFromHost(const nsAString& aHost);
 
   void RemoveOriginsFromOriginAttributesPattern(
       const OriginAttributesPattern& aPattern);
 
   void RemoveOrigins();
 
+  void RemoveOriginsForTTL();
+
+  void MaybeCreateCleanupTimer();
+
+  void MaybeCancelCleanupTimer();
+
   static void LogToConsoleInvalidJSON(nsIHttpChannel* aChannel, nsIURI* aURI);
 
   static void LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel, nsIURI* aURI,
                                          const nsAString& aName);
 
   static void LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel,
                                           nsIURI* aURI);
 
@@ -112,14 +120,16 @@ class ReportingHeader final : public nsI
   static void LogToConsoleInternal(nsIHttpChannel* aChannel, nsIURI* aURI,
                                    const char* aMsg,
                                    const nsTArray<nsString>& aParams);
 
   static void GetEndpointForReportInternal(const ReportingHeader::Group& aGrup,
                                            nsACString& aEndpointURI);
 
   nsClassHashtable<nsCStringHashKey, Client> mOrigins;
+
+  nsCOMPtr<nsITimer> mCleanupTimer;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_ReportingHeader_h
--- a/dom/reporting/tests/browser_cleanup.js
+++ b/dom/reporting/tests/browser_cleanup.js
@@ -1,8 +1,10 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
 const TEST_HOST = "example.org";
 const TEST_DOMAIN = "https://" + TEST_HOST;
 const TEST_PATH = "/browser/dom/reporting/tests/";
 const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "empty.html";
 const TEST_SJS = TEST_DOMAIN + TEST_PATH + "delivering.sjs";
 
 async function storeReportingHeader(browser, extraParams = "") {
   await ContentTask.spawn(browser, { url: TEST_SJS, extraParams }, async obj => {
@@ -16,16 +18,17 @@ async function storeReportingHeader(brow
 
 add_task(async function() {
   await SpecialPowers.flushPrefEnv();
   await SpecialPowers.pushPrefEnv({"set": [
     ["dom.reporting.enabled", true],
     ["dom.reporting.header.enabled", true],
     ["dom.reporting.testing.enabled", true],
     ["dom.reporting.delivering.timeout", 1],
+    ["dom.reporting.cleanup.timeout", 1],
     ["privacy.userContext.enabled", true],
   ]});
 });
 
 add_task(async function() {
   info("Testing a total cleanup");
 
   let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
@@ -161,13 +164,36 @@ add_task(async function() {
   BrowserTestUtils.removeTab(tab);
 
   ContextualIdentityService.remove(identity.userContextId);
 
   ok(!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN + "^userContextId=" + identity.userContextId), "No more data after a container removal");
 });
 
 add_task(async function() {
+  info("TTL cleanup");
+
+  let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+  gBrowser.selectedTab = tab;
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+
+  ok(!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "No data before the test");
+
+  await storeReportingHeader(browser);
+  ok(ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "We have data for the origin");
+
+  // Let's wait a bit.
+  await new Promise(resolve => { setTimeout(resolve, 5000); });
+
+  ok(!ChromeUtils.hasReportingHeaderForOrigin(TEST_DOMAIN), "No data anymore");
+
+  info("Removing the tab");
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function() {
   info("Cleaning up.");
   await new Promise(resolve => {
     Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
   });
 });
--- a/dom/reporting/tests/delivering.sjs
+++ b/dom/reporting/tests/delivering.sjs
@@ -12,17 +12,17 @@ function handleRequest(aRequest, aRespon
   if (aRequest.method == "GET" && params.get("task") == "header") {
     let extraParams = [];
 
     if (params.has("410")) {
       extraParams.push("410=true");
     }
 
     let body = {
-      max_age: 100000,
+      max_age: 1,
       endpoints: [{
         url: "https://example.org/tests/dom/reporting/tests/delivering.sjs" +
                (extraParams.length ? "?" + extraParams.join("&") : ""),
         priority: 1,
         weight: 1,
       }]
     };
 
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -1909,16 +1909,23 @@ VARCACHE_PREF(
 #undef PREF_VALUE
 
 VARCACHE_PREF(
   "dom.reporting.header.enabled",
    dom_reporting_header_enabled,
   RelaxedAtomicBool, false
 )
 
+// In seconds. The timeout to remove not-active report-to endpoints.
+VARCACHE_PREF(
+  "dom.reporting.cleanup.timeout",
+   dom_reporting_cleanup_timeout,
+  uint32_t, 3600
+)
+
 // Any X seconds the reports are dispatched to endpoints.
 VARCACHE_PREF(
   "dom.reporting.delivering.timeout",
    dom_reporting_delivering_timeout,
   uint32_t, 5
 )
 
 // How many times the delivering of a report should be tried.