From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../antitracking/test/xpcshell/data/font.woff | Bin 0 -> 1112 bytes .../components/antitracking/test/xpcshell/head.js | 11 + .../test/xpcshell/test_ExceptionListService.js | 107 +++++ .../test/xpcshell/test_cookie_behavior.js | 94 ++++ .../test/xpcshell/test_getPartitionKeyFromURL.js | 223 +++++++++ .../test/xpcshell/test_purge_trackers.js | 523 +++++++++++++++++++++ .../test/xpcshell/test_purge_trackers_telemetry.js | 175 +++++++ .../test/xpcshell/test_rejectForeignAllowList.js | 116 +++++ .../test/xpcshell/test_staticPartition_authhttp.js | 125 +++++ .../test_staticPartition_clientAuthRemember.js | 129 +++++ .../test/xpcshell/test_staticPartition_font.js | 112 +++++ .../test/xpcshell/test_staticPartition_image.js | 86 ++++ .../test/xpcshell/test_staticPartition_prefetch.js | 177 +++++++ .../test/xpcshell/test_staticPartition_preload.js | 187 ++++++++ .../test/xpcshell/test_tracking_db_service.js | 492 +++++++++++++++++++ .../antitracking/test/xpcshell/test_view_source.js | 78 +++ .../antitracking/test/xpcshell/xpcshell.ini | 51 ++ 17 files changed, 2686 insertions(+) create mode 100644 toolkit/components/antitracking/test/xpcshell/data/font.woff create mode 100644 toolkit/components/antitracking/test/xpcshell/head.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js create mode 100644 toolkit/components/antitracking/test/xpcshell/test_view_source.js create mode 100644 toolkit/components/antitracking/test/xpcshell/xpcshell.ini (limited to 'toolkit/components/antitracking/test/xpcshell') diff --git a/toolkit/components/antitracking/test/xpcshell/data/font.woff b/toolkit/components/antitracking/test/xpcshell/data/font.woff new file mode 100644 index 0000000000..acda4f3d9f Binary files /dev/null and b/toolkit/components/antitracking/test/xpcshell/data/font.woff differ diff --git a/toolkit/components/antitracking/test/xpcshell/head.js b/toolkit/components/antitracking/test/xpcshell/head.js new file mode 100644 index 0000000000..f9bf797641 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../../../components/url-classifier/tests/unit/head_urlclassifier.js */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); diff --git a/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js new file mode 100644 index 0000000000..4063d067f5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js @@ -0,0 +1,107 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +/* Unit tests for the nsIPartitioningExceptionListService implementation. */ + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +do_get_profile(); + +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return new Promise(function (resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +add_task(async _ => { + let peuService = Cc[ + "@mozilla.org/partitioning/exception-list-service;1" + ].getService(Ci.nsIPartitioningExceptionListService); + + // Make sure we have a pref initially, since the exception list service + // requires it. + Services.prefs.setStringPref(PREF_NAME, ""); + + let updateEvent = new UpdateEvent(); + let records = [ + { + id: "1", + last_modified: 1000000000000001, + firstPartyOrigin: "https://example.org", + thirdPartyOrigin: "https://tracking.example.com", + }, + ]; + + // Add some initial data + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), records); + + let promise = waitForEvent(updateEvent, "update"); + let obs = data => { + let event = new CustomEvent("update", { detail: data }); + updateEvent.dispatchEvent(event); + }; + peuService.registerAndRunExceptionListObserver(obs); + let list = await promise; + Assert.equal(list, "", "No items in the list"); + + // Second event is from the RemoteSettings record. + list = await waitForEvent(updateEvent, "update"); + Assert.equal( + list, + "https://example.org,https://tracking.example.com", + "Has one item in the list" + ); + + records.push({ + id: "2", + last_modified: 1000000000000002, + firstPartyOrigin: "https://foo.org", + thirdPartyOrigin: "https://bar.com", + }); + + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + list = await promise; + Assert.equal( + list, + "https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref(PREF_NAME, "https://test.com,https://test3.com"); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref( + PREF_NAME, + "https://test.com,https://test3.com;https://abc.com,https://def.com" + ); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://abc.com,https://def.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + peuService.unregisterExceptionListObserver(obs); + await db.clear(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js new file mode 100644 index 0000000000..3ce1f8bfb7 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js @@ -0,0 +1,94 @@ +/* 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/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const PREF_FPI = "privacy.firstparty.isolate"; +const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior"; +const PREF_COOKIE_BEHAVIOR_PBMODE = "network.cookie.cookieBehavior.pbmode"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_FPI); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); +}); + +add_task(function test_FPI_off() { + Services.prefs.setBoolPref(PREF_FPI, false); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal(Services.cookies.getCookieBehavior(false), i); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal(Services.cookies.getCookieBehavior(true), i); + } +}); + +add_task(function test_FPI_on() { + Services.prefs.setBoolPref(PREF_FPI, true); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal( + Services.cookies.getCookieBehavior(false), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal( + Services.cookies.getCookieBehavior(true), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_FPI); +}); + +add_task(function test_private_cookieBehavior_mirroring() { + // Test that the private cookieBehavior getter will return the regular pref if + // the regular pref has a user value and the private pref has a default value. + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR)) { + continue; + } + + equal(Services.cookies.getCookieBehavior(true), i); + } + + // Test that the private cookieBehavior getter will always return the private + // pref if the private cookieBehavior has a user value. + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR_PBMODE)) { + continue; + } + + for (let j = 0; j <= Ci.nsICookieService.BEHAVIOR_LAST; ++j) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, j); + + equal(Services.cookies.getCookieBehavior(true), i); + } + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js new file mode 100644 index 0000000000..cdf2cdec2c --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +const TEST_CASES = [ + // Tests for different schemes. + { + url: "http://example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for sub domains + { + url: "http://sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://sub.sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for path and query. + { + url: "http://www.example.com/path/to/somewhere/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://www.example.com/?query=string", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for other ports. + { + url: "http://example.com:8080/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com:8080/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for about urls + { + url: "about:about", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + { + url: "about:preferences", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + // Test for ip addresses + { + url: "http://127.0.0.1/", + partitionKeySite: "(http,127.0.0.1)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://127.0.0.1:8080/", + partitionKeySite: "(http,127.0.0.1,8080)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://[2001:db8::ff00:42:8329]", + partitionKeySite: "(http,[2001:db8::ff00:42:8329])", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + { + url: "http://[2001:db8::ff00:42:8329]:8080", + partitionKeySite: "(http,[2001:db8::ff00:42:8329],8080)", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + // Tests for moz-extension + { + url: "moz-extension://bafa4a3f-5c49-48d6-9788-03489419b70e", + partitionKeySite: "", + partitionKeyWithoutSite: "", + }, + // Tests for non tld + { + url: "http://notld", + partitionKeySite: "(http,notld)", + partitionKeyWithoutSite: "notld", + }, + { + url: "http://com", + partitionKeySite: "(http,com)", + partitionKeyWithoutSite: "com", + }, + { + url: "http://com:8080", + partitionKeySite: "(http,com,8080)", + partitionKeyWithoutSite: "com", + }, +]; + +const TEST_INVALID_URLS = [ + "", + "/foo", + "An invalid URL", + "https://", + "http:///", + "http://foo:bar", +]; + +add_task(async function test_get_partition_key_from_url() { + for (const test of TEST_CASES) { + info(`Testing url: ${test.url}`); + let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url); + + Assert.equal( + partitionKey, + test.partitionKeySite, + "The partitionKey is correct." + ); + } +}); + +add_task(async function test_get_partition_key_from_url_without_site() { + Services.prefs.setBoolPref("privacy.dynamic_firstparty.use_site", false); + + for (const test of TEST_CASES) { + info(`Testing url: ${test.url}`); + let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url); + + Assert.equal( + partitionKey, + test.partitionKeyWithoutSite, + "The partitionKey is correct." + ); + } + + Services.prefs.clearUserPref("privacy.dynamic_firstparty.use_site"); +}); + +add_task(async function test_blob_url() { + do_get_profile(); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "

Hello!

"; + response.write(body); + }); + + server.registerPathHandler("/iframe", (metadata, response) => { + var body = ` + + `; + response.write(body); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + let blobUrl = await contentPage.spawn([], async () => { + // Create a third-party iframe and create a blob url in there. + let f = this.content.document.createElement("iframe"); + f.src = "http://foo.com/iframe"; + + let blob_url = await new Promise(resolve => { + this.content.addEventListener("message", event => resolve(event.data), { + once: true, + }); + this.content.document.body.append(f); + }); + + return blob_url; + }); + + let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl); + + // The partitionKey of the blob url is empty because the principal of the + // blob url is the JS principal of the global, which doesn't have + // partitionKey. And ChromeUtils.getPartitionKeyFromURL() will get + // partitionKey from that principal. So, we will get an empty partitionKey + // here. + // XXX: The behavior here is debatable. + Assert.equal(partitionKey, "", "The partitionKey of blob url is correct."); + + await contentPage.close(); +}); + +add_task(async function test_throw_with_invalid_URL() { + // The API should throw if the url is invalid. + for (const invalidURL of TEST_INVALID_URLS) { + info(`Testing invalid url: ${invalidURL}`); + + Assert.throws( + () => { + ChromeUtils.getPartitionKeyFromURL(invalidURL); + }, + /NS_ERROR_MALFORMED_URI/, + "It should fail on invalid URLs." + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js new file mode 100644 index 0000000000..84694a35d5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js @@ -0,0 +1,523 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const TRACKING_PAGE2 = + "https://tracking.example.org^partitionKey=(https,example.com)"; +const BENIGN_PAGE = "https://example.com"; +const FOREIGN_PAGE = "https://example.net"; +const FOREIGN_PAGE2 = "https://example.net^partitionKey=(https,example.com)"; +const FOREIGN_PAGE3 = "https://example.net^partitionKey=(https,example.org)"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +async function setupTest(aCookieBehavior) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", aCookieBehavior); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setCharPref("privacy.purge_trackers.logging.level", "Debug"); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +} + +/** + * Test that purging doesn't happen when it shouldn't happen. + */ +add_task(async function testNotPurging() { + await UrlClassifierTestUtils.addTestTrackers(); + setupTest(Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", false); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); + Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.history"); + + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(!SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie cleared."); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test that cookies indexedDB and localStorage are purged if the cookie is found + * on the tracking list and does not have an Interaction Permission. + */ +async function testIndexedDBAndLocalStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + `A non-tracking foreign page should retain cookies after purging` + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + `localStorage for ${url} should not have been removed.` + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Cookie should have been removed. + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed" + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are treated based on their base domain, not origin. + */ +async function testBaseDomain() { + await UrlClassifierTestUtils.addTestTrackers(); + + let associatedOrigins = [ + "https://itisatracker.org", + "https://sub.itisatracker.org", + "https://www.itisatracker.org", + "https://sub.sub.sub.itisatracker.org", + "http://itisatracker.org", + "http://sub.itisatracker.org", + ]; + + for (let permissionOrigin of associatedOrigins) { + // Only one of the associated origins gets permission, but + // all should be exempt from purging. + PermissionTestUtils.add( + permissionOrigin, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + for (let origin of associatedOrigins) { + SiteDataTestUtils.addToCookies({ origin }); + } + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let origin of associatedOrigins) { + ok( + SiteDataTestUtils.hasCookies(origin), + `${origin} should have retained its cookies when permission is set for ${permissionOrigin}.` + ); + } + + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + + PermissionTestUtils.remove(permissionOrigin, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are not cleared if they are associated + * with an entry on the entity list that has user interaction. + */ +async function testUserInteraction(ownerPage) { + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + true + ); + // The test URL for the entity list for annotation is + // itisatrap.org/?resource=example.org, so we need to + // add example.org as a tracker. + Services.prefs.setCharPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "example.org" + ); + await UrlClassifierTestUtils.addTestTrackers(); + + // example.org and itisatrap.org are hard coded test values on the entity list. + const RESOURCE_PAGE = "https://example.org"; + + PermissionTestUtils.add( + ownerPage, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: RESOURCE_PAGE }); + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ + origin: "https://another-tracking.example.net", + }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should have retained its cookies when permission is set for ${ownerPage}.` + ); + + ok( + !SiteDataTestUtils.hasCookies("https://another-tracking.example.net"), + "cookie is removed after purge with no storage access permission." + ); + + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + false + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + !SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should not have retained its cookies when permission is set for ${ownerPage} and the entity list pref is off.` + ); + + PermissionTestUtils.remove(ownerPage, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + + Services.prefs.clearUserPref("privacy.purge_trackers.consider_entity_list"); + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that quota storage (even without cookies) is considered when purging trackers. + */ +async function testQuotaStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + let testCases = [ + { localStorage: true, indexedDB: true }, + { localStorage: false, indexedDB: true }, + { localStorage: true, indexedDB: false }, + ]; + + for (let { localStorage, indexedDB } of testCases) { + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + if (localStorage) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + } + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + await SiteDataTestUtils.addToIndexedDB(url); + } + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed for non-tracking page." + ); + } + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed." + ); + } + } + + if (indexedDB) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage for non-tracking page was not deleted" + ); + } + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + } + + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that we correctly delete cookies and storage for sites + * with an expired interaction permission. + */ +async function testExpiredInteractionPermission() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + 500 + ); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 500)); + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + // Cookie should not have been removed. + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +add_task(async function () { + const cookieBehaviors = [ + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ]; + + for (let cookieBehavior of cookieBehaviors) { + await setupTest(cookieBehavior); + await testIndexedDBAndLocalStorage(); + await testBaseDomain(); + // example.org and itisatrap.org are hard coded test values on the entity list. + await testUserInteraction("https://itisatrap.org"); + await testUserInteraction( + "https://itisatrap.org^firstPartyDomain=example.net" + ); + await testQuotaStorage(); + await testExpiredInteractionPermission(); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js new file mode 100644 index 0000000000..a1502373dc --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const BENIGN_PAGE = "https://example.com"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +add_task(async function setup() { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +}); + +/** + * Test telemetry for cookie purging. + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + let FIVE_DAYS = 5 * 24 * 60 * 60 * 1000; + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + FIVE_DAYS + ); + + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE); + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + await SiteDataTestUtils.addToIndexedDB(TRACKING_PAGE); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + let notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + let remainingDaysHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS" + ); + let intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + `We have data for ${TRACKING_PAGE}` + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 0, 1); + TelemetryTestUtils.assertHistogram(notPurgedHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(remainingDaysHistogram, 4, 2); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + // Cookie should have been removed. + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + "quota storage was deleted" + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + Assert.equal( + notPurgedHistogram.snapshot().sum, + 0, + "no origins with user interaction" + ); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test counting correctly across cookies batches + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + // Enforce deleting the same origin twice by adding two cookies and setting + // the max number of cookies per batch to 1. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie1" }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie2" }); + Services.prefs.setIntPref("privacy.purge_trackers.max_purge_count", 1); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + await TestUtils.waitForCondition( + () => !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge." + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + + Services.prefs.clearUserPref("privacy.purge_trackers.max_purge_count"); + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js new file mode 100644 index 0000000000..97e95a43f4 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js @@ -0,0 +1,116 @@ +"use strict"; + +do_get_profile(); + +// Let's use XPCShellContentUtils to open/close tabs. +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +var createHttpServer = (...args) => { + return XPCShellContentUtils.createHttpServer(...args); +}; + +const server = createHttpServer({ + hosts: ["3rdparty.org", "4thparty.org", "foobar.com"], +}); + +async function testThings(prefValue, expected) { + await new Promise(resolve => + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_ALL_CACHES, + resolve + ) + ); + + Services.prefs.setCharPref("privacy.rejectForeign.allowList", prefValue); + + let cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyChannel", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(``); + }); + + server.registerPathHandler("/img", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "image/png", false); + response.write("Not an image"); + }); + }); + + // Let's load 3rdparty.org as a 3rd-party. + let contentPage = await XPCShellContentUtils.loadContentPage( + "http://foobar.com/test3rdPartyChannel" + ); + Assert.equal(await cookiePromise, expected, "Cookies received?"); + await contentPage.close(); + + cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyDocument", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write( + ``; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/font-iframe", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + `; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 5, + }, + { + prefValue: false, + // The font in page B/C is CORS, the channel will be flagged with + // nsIRequest::LOAD_ANONYMOUS. + // The flag makes the font in A and B/C use different cache key. + hitsCount: 2, + }, + ]; + + for (let test of tests) { + info("Clear network caches"); + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/font" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/font" + ); + await contentPage.close(); + + info("Let's load a page with origin C"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://bar.com/font" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js new file mode 100644 index 0000000000..7492d2267a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + server.registerPathHandler("/image.png", (metadata, response) => { + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=" + ); + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/image", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = ``; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 2, + }, + { + prefValue: false, + hitsCount: 1, + }, + ]; + + for (let test of tests) { + info("Clear image and network caches"); + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/image" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/image" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js new file mode 100644 index 0000000000..1215ec2384 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +// Small red image. +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "image.png" + ); + ok( + foundEntryCount > 0, + `Cache entries expected for image.png and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.prefetch-next", true); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/image.png", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + response.write(IMG_BYTES); + }); + + server.registerPathHandler("/prefetch", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = ``; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + originAttributes: { partitionKey: "(http,example.org)" }, + prefValue: true, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js new file mode 100644 index 0000000000..be61a2e6d4 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "style.css" + ); + ok( + foundEntryCount > 0, + `Cache entries expected for style.css and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.preload", true); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "

Hello!

"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/style.css", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Access-Control-Allow-Origin", "*", false); + var body = "* { color: red }"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/preload", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = ``; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + prefValue: true, + originAttributes: { partitionKey: "(http,example.org)" }, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the shared sheets"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + await contentPage.spawn([], () => + // eslint-disable-next-line no-undef + content.windowUtils.clearSharedStyleSheetCache() + ); + + await contentPage.close(); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js new file mode 100644 index 0000000000..0f6ded1218 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js @@ -0,0 +1,492 @@ +/* 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/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +XPCOMUtils.defineLazyGetter(this, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +const SQL = { + insertCustomTimeEvent: + "INSERT INTO events (type, count, timestamp)" + + "VALUES (:type, :count, date(:timestamp));", + + selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;", + + selectAll: "SELECT * FROM events", +}; + +// Emulate the content blocking log. We do not record the url key, nor +// do we use the aggregated event number (the last element in the array). +const LOG = { + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + "https://2.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://3.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2], + ], + "https://4.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3], + ], + "https://5.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1], + ], + // Cookie blocked for other reason, then identified as a tracker + "https://6.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL | + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + true, + 4, + ], + ], + "https://7.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], + "https://8.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], + + // The contents below should not add to the database. + // Cookie loaded but not blocked. + "https://10.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + ], + // Tracker cookie loaded but not blocked. + "https://11.unblocked.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1], + ], + // Social tracker cookie loaded but not blocked. + "https://12.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], + // Cookie blocked for other reason (not a tracker) + "https://13.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2], + ], + // Fingerprinters set to block, but this one has an exception + "https://14.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1], + ], + // Two fingerprinters replaced with a shims script, should be treated as blocked + // and increment the counter. + "https://15.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://16.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], +}; + +do_get_profile(); + +Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true); +Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true +); +Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + true +); +Services.prefs.setBoolPref( + "browser.contentblocking.cfr-milestone.enabled", + true +); +Services.prefs.setIntPref( + "browser.contentblocking.cfr-milestone.update-interval", + 0 +); +Services.prefs.setStringPref( + "browser.contentblocking.cfr-milestone.milestones", + "[1000, 5000, 10000, 25000, 100000, 500000]" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.contentblocking.database.enabled"); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); + Services.prefs.clearUserPref( + "privacy.trackingprotection.fingerprinting.enabled" + ); + Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled"); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.update-interval" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.milestones" + ); +}); + +// This tests that data is added successfully, different types of events should get +// their own entries, when the type is the same they should be aggregated. Events +// that are not blocking events should not be recorded. Cookie blocking events +// should only be recorded if we can identify the cookie as a tracking cookie. +add_task(async function test_save_and_delete() { + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Peek in the DB to make sure we have the right data. + let db = await Sqlite.openConnection({ path: DB_PATH }); + // Make sure the items table was created. + ok(await db.tableExists("events"), "events table exists"); + + // make sure we have the correct contents in the database + let rows = await db.execute(SQL.selectAll); + equal( + rows.length, + 5, + "Events that should not be saved have not been, length is 4" + ); + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 1, "Only one day has had tracker entries, length is 1"); + let count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one tracker entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal(rows.length, 1, "Only one day has had cookies entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 3, "Cookie entries were aggregated"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had cryptominer entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one cryptominer entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had fingerprinters entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 3, "there are three fingerprinter entries"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SOCIAL_ID, + }); + equal(rows.length, 1, "Only one day has had social entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 2, "there are two social entries"); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +// This tests that content blocking events encountered on the same day get aggregated, +// and those on different days get seperate entries +add_task(async function test_timestamp_aggragation() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + let today = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: yesterday, + }); + + // Add some events for today which must get aggregated + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: today, + }); + + // Add new events, they will have today's timestamp. + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Ensure events that are inserted today are not aggregated with past events. + let rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2"); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 4, "Yesterday's count is 4"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 2, + "Cryptominer entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 3, "Yesterday's count is 3"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 2, + "Fingerprinter entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 2, "Yesterday's count is 2"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal( + rows.length, + 2, + "Tracking Cookies entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 1, "Yesterday's count is 1"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +let addEventsToDB = async db => { + let d = new Date(1521009000000); + let date = d.toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + + date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); +}; + +// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time +// and return entries that occur within the timestamps, rounded to the nearest day and inclusive. +add_task(async function test_getEventsByDateRange() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + await addEventsToDB(db); + + let d = new Date(1521009000000); + let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000); + let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000); + + let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d); + equal( + events.length, + 1, + "There is 1 event entry between the date and one day before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange(daysBefore4, d); + equal( + events.length, + 4, + "There is 4 event entries between the date and four days before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange( + daysBefore9, + daysBefore4 + ); + equal( + events.length, + 2, + "There is 2 event entries between nine and four days before, inclusive" + ); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.sumAllEvents returns the number of +// tracking events in the database, and can handle 0 entries. +add_task(async function test_sumAllEvents() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let sum = await TrackingDBService.sumAllEvents(); + equal(sum, 0, "There have been 0 events recorded"); + + // populate the database + await addEventsToDB(db); + + sum = await TrackingDBService.sumAllEvents(); + equal(sum, 11, "There have been 11 events recorded"); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.getEarliestRecordedDate returns the +// earliest date recorded and can handle 0 entries. +add_task(async function test_getEarliestRecordedDate() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let timestamp = await TrackingDBService.getEarliestRecordedDate(); + equal(timestamp, null, "There is no earliest recorded date"); + + // populate the database + await addEventsToDB(db); + let d = new Date(1521009000000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0]; + + timestamp = await TrackingDBService.getEarliestRecordedDate(); + let date = new Date(timestamp).toISOString().split("T")[0]; + equal(date, daysBefore9, "The earliest recorded event is nine days before."); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone +add_task(async function test_sendMilestoneNotification() { + let milestones = JSON.parse( + Services.prefs.getStringPref( + "browser.contentblocking.cfr-milestone.milestones" + ) + ); + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + // save number of trackers equal to the first milestone + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: milestones[0], + timestamp: new Date().toISOString(), + }); + + let awaitNotification = TestUtils.topicObserved( + "SiteProtection:ContentBlockingMilestone" + ); + + // trigger a "save" event to compare the trackers with the milestone. + await TrackingDBService.saveEvents( + JSON.stringify({ + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + }) + ); + await awaitNotification; + + await TrackingDBService.clearAll(); + await db.close(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_view_source.js b/toolkit/components/antitracking/test/xpcshell/test_view_source.js new file mode 100644 index 0000000000..ebd70cf476 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_view_source.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gCookieHits = 0; +let gLoadingHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org"], + }); + server.registerPathHandler("/test", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + if ( + request.hasHeader("Cookie") && + request.getHeader("Cookie") == "foo=bar" + ) { + gCookieHits++; + } else { + response.setHeader("Set-Cookie", "foo=bar"); + } + + gLoadingHits++; + var body = ""; + response.bodyOutputStream.write(body, body.length); + }); + + info("Reset the hits count"); + gCookieHits = 0; + gLoadingHits = 0; + + info("Let's load a page"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info("Let's load the source of the page again to see if it loads from cache"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info( + "Let's load the source of the page without hitting the cache to see if the cookie is sent properly" + ); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?2" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 1, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 2, "The number of loading hits match"); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/xpcshell.ini b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..b895aa8e07 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini @@ -0,0 +1,51 @@ +[DEFAULT] +head = head.js ../../../../components/url-classifier/tests/unit/head_urlclassifier.js +prefs = + dom.security.https_first=false #Disable https-first because of explicit http/https testing + +[test_cookie_behavior.js] +[test_getPartitionKeyFromURL.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_purge_trackers.js] +skip-if = + win10_2004 # Bug 1718292 + win10_2009 # Bug 1718292 + win11_2009 # Bug 1797751 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +run-sequentially = very high failure rate in parallel + +[test_purge_trackers_telemetry.js] +[test_tracking_db_service.js] +skip-if = toolkit == "android" # Bug 1697936 +[test_rejectForeignAllowList.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_clientAuthRemember.js] +[test_staticPartition_font.js] +support-files = + data/font.woff +skip-if = + os == "linux" && !debug # Bug 1760086 + apple_silicon # bug 1729551 + os == "mac" && bits == 64 && !debug # Bug 1652119 + os == "win" && bits == 64 && !debug # Bug 1652119 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + socketprocess_networking # Bug 1759035 +run-sequentially = very high failure rate in parallel +[test_staticPartition_image.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_authhttp.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_prefetch.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_preload.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_ExceptionListService.js] +[test_view_source.js] +skip-if = + socketprocess_networking # Bug 1759035 (not as common on win, perma on linux/osx) -- cgit v1.2.3