From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../test/browser/browser.toml | 20 ++ .../browser/browser_bouncetracking_oa_isolation.js | 73 +++++ .../test/browser/browser_bouncetracking_purge.js | 121 ++++++++ .../test/browser/browser_bouncetracking_simple.js | 89 ++++++ .../browser/browser_bouncetracking_stateful.js | 63 +++++ .../test/browser/file_bounce.html | 59 ++++ .../test/browser/file_bounce.sjs | 19 ++ .../test/browser/file_start.html | 11 + .../bouncetrackingprotection/test/browser/head.js | 275 ++++++++++++++++++ .../test/marionette/manifest.toml | 7 + .../test_bouncetracking_storage_persistence.py | 133 +++++++++ .../test/xpcshell/test_bouncetracking_purge.js | 307 +++++++++++++++++++++ .../test/xpcshell/xpcshell.toml | 8 + 13 files changed, 1185 insertions(+) create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml (limited to 'toolkit/components/antitracking/bouncetrackingprotection/test') diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml new file mode 100644 index 0000000000..1c44d7804e --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml @@ -0,0 +1,20 @@ +[DEFAULT] +head = "head.js" +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] +support-files = [ + "file_start.html", + "file_bounce.sjs", + "file_bounce.html", +] + +["browser_bouncetracking_oa_isolation.js"] + +["browser_bouncetracking_purge.js"] + +["browser_bouncetracking_simple.js"] + +["browser_bouncetracking_stateful.js"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js new file mode 100644 index 0000000000..12c2c943dd --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_pbm_data_isolated() { + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + originAttributes: { privateBrowsingId: 1 }, + postBounceCallback: () => { + // After the PBM bounce assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + }, + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_containers_isolated() { + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + originAttributes: { userContextId: 2 }, + postBounceCallback: () => { + // After the bounce in the container tab assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + + // Or in another container tab. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ + userContextId: 1, + }).length, + 0, + "No bounce tracker candidates for container tab 1." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({ + userContextId: 1, + }).length, + 0, + "No user activations for container tab 1." + ); + }, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js new file mode 100644 index 0000000000..a8e98b80f0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BOUNCE_TRACKING_GRACE_PERIOD_SEC = 30; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", + BOUNCE_TRACKING_GRACE_PERIOD_SEC, + ], + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ], + }); +}); + +/** + * The following tests ensure that sites that have open tabs are exempt from purging. + */ + +function initBounceTrackerState() { + bounceTrackingProtection.clearAll(); + + // Bounce time of 1 is out of the grace period which means we should purge. + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.net", 1); + + // Should not purge because within grace period. + let timestampWithinGracePeriod = + Date.now() - (BOUNCE_TRACKING_GRACE_PERIOD_SEC * 1000) / 2; + bounceTrackingProtection.testAddBounceTrackerCandidate( + {}, + "example.org", + timestampWithinGracePeriod * 1000 + ); +} + +add_task(async function test_purging_skip_open_foreground_tab() { + initBounceTrackerState(); + + // Foreground tab + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_background_tab() { + initBounceTrackerState(); + + // Background tab + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_tab_extra_window() { + initBounceTrackerState(); + + // Foreground tab in new window. + let win = await BrowserTestUtils.openNewBrowserWindow({}); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info( + "Close the window with the tab for example.com and test that it gets purged now." + ); + initBounceTrackerState(); + + await BrowserTestUtils.closeWindow(win); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js new file mode 100644 index 0000000000..dfbd4d0fc0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests a stateless bounce via client redirect. +add_task(async function test_client_bounce_simple() { + await runTestBounce({ bounceType: "client" }); +}); + +// Tests a stateless bounce via server redirect. +add_task(async function test_server_bounce_simple() { + await runTestBounce({ bounceType: "server" }); +}); + +// Tests a chained redirect consisting of a server and a client redirect. +add_task(async function test_bounce_chain() { + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially." + ); + + await BrowserTestUtils.withNewTab( + getBaseUrl(ORIGIN_A) + "file_start.html", + async browser => { + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounces. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Construct last hop. + let bounceChainUrlEnd = getBounceURL({ bounceType: "server", targetURL }); + // Construct first hop, nesting last hop. + let bounceChainUrlFull = getBounceURL({ + bounceType: "client", + redirectDelayMS: 100, + bounceOrigin: ORIGIN_TRACKER_B, + targetURL: bounceChainUrlEnd, + }); + + info("bounceChainUrl: " + bounceChainUrlFull.href); + + // Navigate through the bounce chain. + await navigateLinkClick(browser, bounceChainUrlFull); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(), + [SITE_TRACKER_B, SITE_TRACKER].sort(), + `Identified all bounce trackers in the redirect chain.` + ); + Assert.deepEqual( + bounceTrackingProtection.testGetUserActivationHosts({}).sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + bounceTrackingProtection.clearAll(); + } + ); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js new file mode 100644 index 0000000000..e7fb4521a7 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Cookie tests. + +add_task(async function test_bounce_stateful_cookies_client() { + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + }); + info("Test client bounce without cookie."); + await runTestBounce({ + bounceType: "client", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +add_task(async function test_bounce_stateful_cookies_server() { + info("Test server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + }); + info("Test server bounce without cookie."); + await runTestBounce({ + bounceType: "server", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +// Storage tests. + +// TODO: Bug 1848406: Implement stateful bounce detection for localStorage. +add_task(async function test_bounce_stateful_localStorage() { + info("TODO: client bounce with localStorage."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + expectCandidate: false, + expectPurge: false, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html new file mode 100644 index 0000000000..2756555fa5 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html @@ -0,0 +1,59 @@ + + + + + + Bounce! + + + +

Nothing to see here...

+ + + diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs new file mode 100644 index 0000000000..5e948a899b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs @@ -0,0 +1,19 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = new URLSearchParams(request.queryString); + + let setState = query.get("setState"); + if (setState == "cookie-server") { + response.setHeader("Set-Cookie", "foo=bar"); + } + + let statusCode = 302; + let statusCodeQuery = query.get("statusCode"); + if (statusCodeQuery) { + statusCode = Number.parseInt(statusCodeQuery); + } + + response.setStatusLine("1.1", statusCode, "Found"); + response.setHeader("Location", query.get("target"), false); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html new file mode 100644 index 0000000000..ded691023b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html @@ -0,0 +1,11 @@ + + + + + + Blank + + + + + diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js new file mode 100644 index 0000000000..f5857b6919 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js @@ -0,0 +1,275 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SITE_A = "example.com"; +const ORIGIN_A = `https://${SITE_A}`; + +const SITE_B = "example.org"; +const ORIGIN_B = `https://${SITE_B}`; + +const SITE_C = "example.net"; +const ORIGIN_C = `https://${SITE_C}`; + +const SITE_TRACKER = "itisatracker.org"; +const ORIGIN_TRACKER = `https://${SITE_TRACKER}`; + +const SITE_TRACKER_B = "trackertest.org"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const ORIGIN_TRACKER_B = `http://${SITE_TRACKER_B}`; + +// Test message used for observing when the record-bounces method in +// BounceTrackingProtection.cpp has finished. +const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished"; + +const ROOT_DIR = getRootDirectory(gTestPath); + +XPCOMUtils.defineLazyServiceGetter( + this, + "bounceTrackingProtection", + "@mozilla.org/bounce-tracking-protection;1", + "nsIBounceTrackingProtection" +); + +/** + * Get the base url for the current test directory using the given origin. + * @param {string} origin - Origin to use in URL. + * @returns {string} - Generated URL as a string. + */ +function getBaseUrl(origin) { + return ROOT_DIR.replace("chrome://mochitests/content", origin); +} + +/** + * Constructs a url for an intermediate "bounce" hop which represents a tracker. + * @param {*} options - URL generation options. + * @param {('server'|'client')} options.bounceType - Redirect type to use for + * the bounce. + * @param {string} [options.bounceOrigin] - The origin of the bounce URL. + * @param {string} [options.targetURL] - URL to redirect to after the bounce. + * @param {("cookie"|null)} [options.setState] - What type of state should be set during + * the bounce. No state by default. + * @param {number} [options.statusCode] - HTTP status code to use for server + * side redirect. Only applies to bounceType == "server". + * @param {number} [options.redirectDelayMS] - How long to wait before + * redirecting. Only applies to bounceType == "client". + * @returns {URL} Generated URL which points to an endpoint performing the + * redirect. + */ +function getBounceURL({ + bounceType, + bounceOrigin = ORIGIN_TRACKER, + targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"), + setState = null, + statusCode = 302, + redirectDelayMS = 50, +}) { + if (!["server", "client"].includes(bounceType)) { + throw new Error("Invalid bounceType"); + } + + let bounceFile = + bounceType == "client" ? "file_bounce.html" : "file_bounce.sjs"; + + let bounceUrl = new URL(getBaseUrl(bounceOrigin) + bounceFile); + + let { searchParams } = bounceUrl; + searchParams.set("target", targetURL.href); + if (setState) { + searchParams.set("setState", setState); + } + + if (bounceType == "server") { + searchParams.set("statusCode", statusCode); + } else if (bounceType == "client") { + searchParams.set("redirectDelay", redirectDelayMS); + } + + return bounceUrl; +} + +/** + * Insert an element with the given target and perform a synthesized + * click on it. + * @param {MozBrowser} browser - Browser to insert the link in. + * @param {URL} targetURL - Destination for navigation. + * @returns {Promise} Resolves once the click is done. Does not wait for + * navigation or load. + */ +async function navigateLinkClick(browser, targetURL) { + await SpecialPowers.spawn(browser, [targetURL.href], targetURL => { + let link = content.document.createElement("a"); + + link.href = targetURL; + link.textContent = targetURL; + // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't + // hit it. + link.style.display = "block"; + + content.document.body.appendChild(link); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser); +} + +/** + * Wait for the record-bounces method to run for the given tab / browser. + * @param {browser} browser - Browser element which represents the tab we want + * to observe. + * @returns {Promise} Promise which resolves once the record-bounces method has + * run for the given browser. + */ +async function waitForRecordBounces(browser) { + return TestUtils.topicObserved( + OBSERVER_MSG_RECORD_BOUNCES_FINISHED, + subject => { + // Ensure the message was dispatched for the browser we're interested in. + let propBag = subject.QueryInterface(Ci.nsIPropertyBag2); + let browserId = propBag.getProperty("browserId"); + return browser.browsingContext.browserId == browserId; + } + ); +} + +/** + * Test helper which loads an initial blank page, then navigates to a url which + * performs a bounce. Checks that the bounce hosts are properly identified as + * trackers. + * @param {object} options - Test Options. + * @param {('server'|'client')} options.bounceType - Whether to perform a client + * or server side redirect. + * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] + * Type of state to set during the redirect. Defaults to non stateful redirect. + * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be + * identified as a bounce tracker (candidate). + * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have + * its storage purged. + * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes + * to use for the test. This determines whether the test is run in normal + * browsing, a private window or a container tab. By default the test is run + * in normal browsing. + * @param {function} [options.postBounceCallback] - Optional function to run after the + * bounce has completed. + */ +async function runTestBounce(options = {}) { + let { + bounceType, + setState = null, + expectCandidate = true, + expectPurge = true, + originAttributes = {}, + postBounceCallback = () => {}, + } = options; + info(`runTestBounce ${JSON.stringify(options)}`); + + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + 0, + "No user activation hosts initially." + ); + + let win = window; + let { privateBrowsingId, userContextId } = originAttributes; + let usePrivateWindow = + privateBrowsingId != null && + privateBrowsingId != + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID; + if (userContextId != null && userContextId > 0 && usePrivateWindow) { + throw new Error("userContextId is not supported in private windows"); + } + + if (usePrivateWindow) { + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + } + + let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + userContextId, + }); + win.gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounce. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Navigate through the bounce chain. + await navigateLinkClick( + browser, + getBounceURL({ bounceType, targetURL, setState }) + ); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ), + expectCandidate ? [SITE_TRACKER] : [], + `Should ${ + expectCandidate ? "" : "not " + }have identified ${SITE_TRACKER} as a bounce tracker.` + ); + Assert.deepEqual( + bounceTrackingProtection + .testGetUserActivationHosts(originAttributes) + .sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + // If the caller specified a function to run after the bounce, run it now. + await postBounceCallback(); + + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + expectPurge ? [SITE_TRACKER] : [], + `Should ${expectPurge ? "" : "not "}purge state for ${SITE_TRACKER}.` + ); + + // Clean up + BrowserTestUtils.removeTab(tab); + if (usePrivateWindow) { + await BrowserTestUtils.closeWindow(win); + + info( + "Closing the last PBM window should trigger a purge of all PBM state." + ); + Assert.ok( + !bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + "No bounce tracker hosts after closing private window." + ); + Assert.ok( + !bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + "No user activation hosts after closing private window." + ); + } + bounceTrackingProtection.clearAll(); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml new file mode 100644 index 0000000000..7caad6eb15 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml @@ -0,0 +1,7 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", +] + +["test_bouncetracking_storage_persistence.py"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py new file mode 100644 index 0000000000..afc3239839 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py @@ -0,0 +1,133 @@ +# 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/. + +from marionette_harness import MarionetteTestCase + +# Tests the persistence of the bounce tracking protection storage across +# restarts. + + +class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): + def setUp(self): + super(BounceTrackingStoragePersistenceTestCase, self).setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.bounceTrackingProtection.enabled": True, + "privacy.bounceTrackingProtection.enableTestMode": True, + } + ) + + self.marionette.set_context("chrome") + self.populate_state() + + def populate_state(self): + # Add some data to test persistence. + self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.net", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.org", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({ userContextId: 3 }, "tracker.com", Date.now() * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddBounceTrackerCandidate({ privateBrowsingId: 1 }, "tracker.net", Date.now() * 10000); + + bounceTrackingProtection.testAddUserActivation({}, "example.com", (Date.now() + 5000) * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddUserActivation({ privateBrowsingId: 1 }, "example.org", (Date.now() + 2000) * 10000); + """ + ) + + def test_state_after_restart(self): + self.marionette.restart(clean=False, in_app=True) + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 2, + msg="There should be two entries for default OA", + ) + self.assertEqual(bounceTrackerCandidates[0], "bouncetracker.net") + self.assertEqual(bounceTrackerCandidates[1], "bouncetracker.org") + + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 1, + msg="There should be only one entry for user context 3", + ) + self.assertEqual(bounceTrackerCandidates[0], "tracker.com") + + # Unrelated user context should not have any entries. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 4 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for user context 4", + ) + + # Private browsing entries should not be persisted across restarts. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for private browsing", + ) + + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({}).sort(); + """, + ) + self.assertEqual( + len(userActivations), + 1, + msg="There should be only one entry for user activation", + ) + self.assertEqual(userActivations[0], "example.com") + + # Private browsing entries should not be persisted across restarts. + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + userActivations, 0, msg="There should be no entries for private browsing" + ) diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js new file mode 100644 index 0000000000..5ede57a08b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +let btp; +let bounceTrackingGracePeriodSec; +let bounceTrackingActivationLifetimeSec; + +/** + * Adds brackets to a host if it's an IPv6 address. + * @param {string} host - Host which may be an IPv6. + * @returns {string} bracketed IPv6 or host if host is not an IPv6. + */ +function maybeFixupIpv6(host) { + if (!host.includes(":")) { + return host; + } + return `[${host}]`; +} + +/** + * Adds cookies and indexedDB test data for the given host. + * @param {string} host + */ +async function addStateForHost(host) { + info(`adding state for host ${host}`); + SiteDataTestUtils.addToCookies({ host }); + await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`); +} + +/** + * Checks if the given host as cookies or indexedDB data. + * @param {string} host + * @returns {boolean} + */ +async function hasStateForHost(host) { + let origin = `https://${maybeFixupIpv6(host)}`; + if (SiteDataTestUtils.hasCookies(origin)) { + return true; + } + return SiteDataTestUtils.hasIndexedDB(origin); +} + +/** + * Assert that there are no bounce tracker candidates or user activations + * recorded. + */ +function assertEmpty() { + Assert.equal( + btp.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No tracker candidates." + ); + Assert.equal( + btp.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts." + ); +} + +add_setup(function () { + // Need a profile to data clearing calls. + do_get_profile(); + + btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + // Reset global bounce tracking state. + btp.clearAll(); + + bounceTrackingGracePeriodSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec" + ); + bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec" + ); +}); + +/** + * When both maps are empty running PurgeBounceTrackers should be a no-op. + */ +add_task(async function test_empty() { + assertEmpty(); + + info("Run PurgeBounceTrackers"); + await btp.testRunPurgeBounceTrackers(); + + assertEmpty(); +}); + +/** + * Tests that the PurgeBounceTrackers behaves as expected by adding site state + * and adding simulated bounce state and user activations. + */ +add_task(async function test_purge() { + let now = Date.now(); + + // Epoch in MS. + let timestampWithinGracePeriod = + now - (bounceTrackingGracePeriodSec * 1000) / 2; + let timestampWithinGracePeriod2 = + now - (bounceTrackingGracePeriodSec * 1000) / 4; + let timestampOutsideGracePeriodFiveSeconds = + now - (bounceTrackingGracePeriodSec + 5) * 1000; + let timestampOutsideGracePeriodThreeDays = + now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000; + let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2; + + let timestampValidUserActivation = + now - (bounceTrackingActivationLifetimeSec * 1000) / 2; + let timestampExpiredUserActivationFourSeconds = + now - (bounceTrackingActivationLifetimeSec + 4) * 1000; + let timestampExpiredUserActivationTenDays = + now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000; + + const TEST_TRACKERS = { + "example.com": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example2.com": { + bounceTime: timestampWithinGracePeriod2, + userActivationTime: null, + message: "Should not purge within grace period (2).", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example.net": { + bounceTime: timestampOutsideGracePeriodFiveSeconds, + userActivationTime: null, + message: "Should purge after grace period.", + shouldPurge: true, + }, + // Also ensure that clear data calls with IP sites succeed. + "1.2.3.4": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (2).", + shouldPurge: true, + }, + "2606:4700:4700::1111": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (3).", + shouldPurge: true, + }, + "example.org": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: false, + }, + "example2.org": { + bounceTime: timestampFuture, + userActivationTime: null, + message: "Should not purge for future bounce time (within grace period).", + shouldPurge: false, + }, + "1.1.1.1": { + bounceTime: null, + userActivationTime: timestampValidUserActivation, + message: "Should not purge without bounce (valid user activation).", + shouldPurge: false, + }, + // Also testing domains with trailing ".". + "mozilla.org.": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationFourSeconds, + message: "Should not purge without bounce (expired user activation).", + shouldPurge: false, + }, + "firefox.com": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationTenDays, + message: "Should not purge without bounce (expired user activation) (2).", + shouldPurge: false, + }, + }; + + info("Assert empty initially."); + assertEmpty(); + + info("Populate bounce and user activation sets."); + + let expectedBounceTrackerHosts = []; + let expectedUserActivationHosts = []; + + let expiredUserActivationHosts = []; + let expectedPurgedHosts = []; + + // This would normally happen over time while browsing. + let initPromises = Object.entries(TEST_TRACKERS).map( + async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => { + // Add site state so we can later assert it has been purged. + await addStateForHost(siteHost); + + if (bounceTime != null) { + if (userActivationTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedBounceTrackerHosts.push(siteHost); + + // Convert bounceTime timestamp to nanoseconds (PRTime). + info( + `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms` + ); + btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000); + } + + if (userActivationTime != null) { + if (bounceTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedUserActivationHosts.push(siteHost); + if ( + userActivationTime + bounceTrackingActivationLifetimeSec * 1000 > + now + ) { + expiredUserActivationHosts.push(siteHost); + } + + // Convert userActivationTime timestamp to nanoseconds (PRTime). + info( + `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms` + ); + btp.testAddUserActivation({}, siteHost, userActivationTime * 1000); + } + + if (shouldPurge) { + expectedPurgedHosts.push(siteHost); + } + } + ); + await Promise.all(initPromises); + + info( + "Check that bounce and user activation data has been correctly recorded." + ); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHosts.sort(), + "Has added bounce tracker hosts." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expectedUserActivationHosts.sort(), + "Has added user activation hosts." + ); + + info("Run PurgeBounceTrackers"); + let actualPurgedHosts = await btp.testRunPurgeBounceTrackers(); + + Assert.deepEqual( + actualPurgedHosts.sort(), + expectedPurgedHosts.sort(), + "Should have purged all expected hosts." + ); + + let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts + .filter(host => !expectedPurgedHosts.includes(host)) + .sort(); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHostsAfterPurge.sort(), + "After purge the bounce tracker candidate host set should be updated correctly." + ); + + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expiredUserActivationHosts.sort(), + "After purge any expired user activation records should have been removed" + ); + + info("Test that we actually purged the correct sites."); + for (let siteHost of expectedPurgedHosts) { + Assert.ok( + !(await hasStateForHost(siteHost)), + `Site ${siteHost} should no longer have state.` + ); + } + for (let siteHost of expectedBounceTrackerHostsAfterPurge) { + Assert.ok( + await hasStateForHost(siteHost), + `Site ${siteHost} should still have state.` + ); + } + + info("Reset bounce tracking state."); + btp.clearAll(); + assertEmpty(); + + info("Clean up site data."); + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..16e270b85c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml @@ -0,0 +1,8 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] + +["test_bouncetracking_purge.js"] -- cgit v1.2.3