diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/resistfingerprinting/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/resistfingerprinting/test')
91 files changed, 10624 insertions, 0 deletions
diff --git a/browser/components/resistfingerprinting/test/browser/browser.ini b/browser/components/resistfingerprinting/test/browser/browser.ini new file mode 100644 index 0000000000..d909b06af6 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser.ini @@ -0,0 +1,95 @@ +[DEFAULT] +prefs = + # bug 1803611 + dom.window_position_size_properties_replaceable.enabled=false + privacy.resistFingerprinting.testing.setTZtoUTC=false +tags = resistfingerprinting +support-files = + coop_header.sjs + file_dummy.html + file_keyBoardEvent.sjs + file_navigator.html + file_navigatorWorker.js + file_workerNetInfo.js + file_workerPerformance.js + head.js + file_navigator_header.sjs + file_navigator_iframer.html + file_navigator_iframee.html + file_navigator_iframe_worker.sjs + file_hwconcurrency_aboutblank_iframer.html + file_hwconcurrency_aboutblank_iframee.html + file_hwconcurrency_aboutblank_popupmaker.html + file_hwconcurrency_aboutsrcdoc_iframer.html + file_hwconcurrency_aboutsrcdoc_iframee.html + file_hwconcurrency_blob_iframer.html + file_hwconcurrency_blob_iframee.html + file_hwconcurrency_blob_popupmaker.html + file_hwconcurrency_blobcrossorigin_iframer.html + file_hwconcurrency_blobcrossorigin_iframee.html + file_hwconcurrency_data_iframee.html + file_hwconcurrency_data_iframer.html + file_hwconcurrency_data_popupmaker.html + file_hwconcurrency_iframer.html + file_hwconcurrency_iframee.html + file_hwconcurrency_sandboxediframe_double_framee.html + file_hwconcurrency_sandboxediframe_iframer.html + file_hwconcurrency_sandboxediframe_iframee.html + file_reduceTimePrecision_iframer.html + file_reduceTimePrecision_iframee.html + file_reduceTimePrecision_iframe_worker.sjs + file_animationapi_iframer.html + file_animationapi_iframee.html + shared_test_funcs.js + +[browser_animationapi_iframes.js] +[browser_block_mozAddonManager.js] +[browser_bug1369357_site_specific_zoom_level.js] +https_first_disabled = true +[browser_cross_origin_isolated_animation_api.js] +[browser_cross_origin_isolated_performance_api.js] +[browser_cross_origin_isolated_reduce_time_precision.js] +[browser_dynamical_window_rounding.js] +https_first_disabled = true +skip-if = + (os == "mac") #Bug 1570812 + os == 'linux' && bits == 64 && !debug # Bug 1570812 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_hwconcurrency_etp_iframes.js] +[browser_hwconcurrency_iframes.js] +[browser_hwconcurrency_iframes_aboutblank.js] +[browser_hwconcurrency_iframes_aboutsrcdoc.js] +[browser_hwconcurrency_iframes_blob.js] +[browser_hwconcurrency_iframes_blobcrossorigin.js] +[browser_hwconcurrency_iframes_data.js] +[browser_hwconcurrency_iframes_sandboxediframe.js] +[browser_hwconcurrency_popups.js] +[browser_hwconcurrency_popups_aboutblank.js] +[browser_hwconcurrency_popups_blob.js] +[browser_hwconcurrency_popups_blob_noopener.js] +[browser_hwconcurrency_popups_data.js] +[browser_hwconcurrency_popups_data_noopener.js] +[browser_hwconcurrency_popups_noopener.js] +[browser_math.js] +[browser_navigator.js] +https_first_disabled = true +skip-if = + os == "win" && bits == 32 # fails on win10-32 +[browser_navigator_iframes.js] +https_first_disabled = true +skip-if = + os == "win" && bits == 32 # fails on win10-32 also +[browser_netInfo.js] +https_first_disabled = true +[browser_performanceAPI.js] +[browser_performanceAPIWorkers.js] +[browser_reduceTimePrecision_iframes.js] +https_first_disabled = true +[browser_roundedWindow_dialogWindow.js] +[browser_roundedWindow_newWindow.js] +[browser_roundedWindow_open_max_inner.js] +[browser_roundedWindow_open_mid_inner.js] +[browser_roundedWindow_open_min_inner.js] +[browser_spoofing_keyboard_event.js] +skip-if = (debug || asan) && os == "linux" && bits == 64 #Bug 1518179 +[browser_timezone.js] diff --git a/browser/components/resistfingerprinting/test/browser/browser_animationapi_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_animationapi_iframes.js new file mode 100644 index 0000000000..29080ce3f0 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_animationapi_iframes.js @@ -0,0 +1,235 @@ +/** + * This test only tests values in the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + */ + +"use strict"; + +requestLongerTimeout(3); + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", +}); + +// ============================================================================================= +// ============================================================================================= + +async function testTimePrecision(results, expectedResults, extraData) { + let testDesc = extraData.testDesc; + let precision = undefined; + + if (!expectedResults.shouldRFPApply) { + precision = extraData.Unconditional_Precision; + } else { + precision = extraData.RFP_Precision; + } + + for (let result of results) { + if ("error" in result) { + ok(false, result.error); + continue; + } + + let isRounded = isTimeValueRounded(result.value, precision); + + ok( + isRounded, + "Test: " + + testDesc + + " - '" + + "'" + + result.name + + "' should be rounded to nearest " + + precision + + " ms; saw " + + result.value + ); + } +} + +const RFP_TIME_ATOM_MS = 16.667; +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html`; + +// The first three variables are defined here; and then set for test banks below. +let extraData = {}; +let extraPrefs = {}; +let precision = 100; +let expectedResults = {}; // In this test, we don't have explicit expected values, but rather we expect them to be rounded + +// ======================================================================================================================== +// Create a function that defines all the tests +function addAllTests(extraData_, extraPrefs_) { + add_task( + defaultsTest.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + add_task( + simpleRFPTest.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + add_task( + testA.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + add_task( + testB.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + add_task( + testC.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + add_task( + testD.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + add_task( + testE.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + add_task( + testF.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + add_task( + testG.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + add_task( + testH.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); +} + +// ======================================================================================================================== +// First we run through all the tests with RTP's precision set to 100 ms and 133 ms. +// Animation does _not_ obey RTP's timestamp, instead it falls back to the unconditional +// rounding which is 20 microseconds. +extraData = { + RFP_Precision: 100, + Unconditional_Precision: 0.02, +}; + +extraPrefs = [ + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + extraData.RFP_Precision * 1000, + ], + ["dom.animations-api.timelines.enabled", true], +]; + +addAllTests(extraData, extraPrefs); + +extraData = { + RFP_Precision: 133, + Unconditional_Precision: 0.02, +}; + +extraPrefs = [ + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + extraData.RFP_Precision * 1000, + ], + ["dom.animations-api.timelines.enabled", true], +]; + +addAllTests(extraData, extraPrefs); + +// ======================================================================================================================== +// Then we run through all the tests with the precision set to its normal value. +// This will mean that in some cases we expect RFP to apply and in some we don't. +extraData = { + RFP_Precision: RFP_TIME_ATOM_MS, + Unconditional_Precision: 0.02, +}; +extraPrefs = [["dom.animations-api.timelines.enabled", true]]; +addAllTests(extraData, extraPrefs); diff --git a/browser/components/resistfingerprinting/test/browser/browser_block_mozAddonManager.js b/browser/components/resistfingerprinting/test/browser/browser_block_mozAddonManager.js new file mode 100644 index 0000000000..e9f55ef667 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_block_mozAddonManager.js @@ -0,0 +1,43 @@ +/** + * Bug 1384330 - A test case for making sure the navigator.mozAddonManager will + * be blocked when pref 'privacy.resistFingerprinting.block_mozAddonManager' is true. + */ + +const HTTPS_TEST_PATH = + "https://example.com/browser/browser/" + + "components/resistfingerprinting/test/browser/"; + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.webapi.testing", true]], + }); + + for (let pref of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting.block_mozAddonManager", pref]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + HTTPS_TEST_PATH + "file_dummy.html" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [pref], function (aPref) { + if (aPref) { + is( + content.navigator.mozAddonManager, + undefined, + "The navigator.mozAddonManager should not exist when the pref is on." + ); + } else { + ok( + content.navigator.mozAddonManager, + "The navigator.mozAddonManager should exist when the pref is off." + ); + } + }); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); + } +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js b/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js new file mode 100644 index 0000000000..f549c71bf0 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_bug1369357_site_specific_zoom_level.js @@ -0,0 +1,77 @@ +"use strict"; + +const PATH_NET = TEST_PATH + "file_dummy.html"; +const PATH_ORG = PATH_NET.replace("example.net", "example.org"); + +add_task(async function () { + let tab1, tab1Zoom, tab2, tab2Zoom, tab3, tab3Zoom; + + tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_NET); + await FullZoom.enlarge(); + tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser); + + tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_NET); + tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser); + + is( + tab2Zoom, + tab1Zoom, + "privacy.resistFingerprinting is false, site-specific zoom level should be enabled" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_NET); + tab3Zoom = ZoomManager.getZoomForBrowser(tab3.linkedBrowser); + + isnot( + tab3Zoom, + tab1Zoom, + "privacy.resistFingerprinting is true, site-specific zoom level should be disabled" + ); + + await FullZoom.reset(); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function exempt_domain() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_NET); + await FullZoom.enlarge(); + const netZoomOriginal = ZoomManager.getZoomForBrowser(tab.linkedBrowser); + is(netZoomOriginal, 1.1, "Initial zoom is 110%"); + await BrowserTestUtils.removeTab(tab); + + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_ORG); + await FullZoom.enlarge(); + const orgZoomOriginal = ZoomManager.getZoomForBrowser(tab.linkedBrowser); + is(orgZoomOriginal, 1.1, "Initial zoom is 110%"); + await BrowserTestUtils.removeTab(tab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.exemptedDomains", "example.net"], + ["privacy.resistFingerprinting", true], + ], + }); + + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_NET); + const netZoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser); + is(netZoom, 1.1, "exempted example.net tab should have kept zoom level"); + await FullZoom.reset(); + await BrowserTestUtils.removeTab(tab); + + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PATH_ORG); + const orgZoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser); + is(orgZoom, 1.0, "example.org tab has its zoom reset to default 100%"); + await FullZoom.reset(); + await BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_animation_api.js b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_animation_api.js new file mode 100644 index 0000000000..f55a492984 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_animation_api.js @@ -0,0 +1,159 @@ +/** + * Bug 1621677 - A test for making sure getting the correct (higher) precision + * when it's cross-origin-isolated on animation APIs. + */ + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the test case for window in +// test_animation_api.html. The main difference is this test case +// verifies animation has more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +add_task(async function runRTPTestAnimation() { + let runTests = async function (data) { + function waitForCondition(aCond, aCallback, aErrorMsg) { + var tries = 0; + var interval = content.setInterval(() => { + if (tries >= 30) { + ok(false, aErrorMsg); + moveOn(); + return; + } + var conditionPassed; + try { + conditionPassed = aCond(); + } catch (e) { + ok(false, `${e}\n${e.stack}`); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = () => { + content.clearInterval(interval); + aCallback(); + }; + } + + let expectedPrecision = data.precision; + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + const testDiv = content.document.getElementById("testDiv"); + const animation = testDiv.animate({ opacity: [0, 1] }, 100000); + animation.play(); + + let done; + let promise = new Promise(resolve => { + done = resolve; + }); + + waitForCondition( + () => animation.currentTime > 100, + () => { + // We have disabled Time Precision Reduction for CSS Animations, so we + // expect those tests to fail. + // If we are testing that preference, we accept either rounded or not + // rounded values as A-OK. + var maybeAcceptEverything = function (value) { + if ( + data.options.reduceTimerPrecision && + !data.options.resistFingerprinting + ) { + return true; + } + + return value; + }; + + ok( + maybeAcceptEverything( + isRounded(animation.startTime, expectedPrecision) + ), + `Animation.startTime with precision ${expectedPrecision} is not ` + + `rounded: ${animation.startTime}` + ); + ok( + maybeAcceptEverything( + isRounded(animation.currentTime, expectedPrecision) + ), + `Animation.currentTime with precision ${expectedPrecision} is ` + + `not rounded: ${animation.currentTime}` + ); + ok( + maybeAcceptEverything( + isRounded(animation.timeline.currentTime, expectedPrecision) + ), + `Animation.timeline.currentTime with precision ` + + `${expectedPrecision} is not rounded: ` + + `${animation.timeline.currentTime}` + ); + if (content.document.timeline) { + ok( + maybeAcceptEverything( + isRounded( + content.document.timeline.currentTime, + expectedPrecision + ) + ), + `Document.timeline.currentTime with precision ` + + `${expectedPrecision} is not rounded: ` + + `${content.document.timeline.currentTime}` + ); + } + done(); + }, + "animation failed to start" + ); + + await promise; + }; + + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 100, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + }, + 50, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + }, + 0.1, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 0.013, + runTests + ); + + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 0.005, + runTests + ); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_performance_api.js b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_performance_api.js new file mode 100644 index 0000000000..fa53e3a1d5 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_performance_api.js @@ -0,0 +1,171 @@ +/** + * Bug 1621677 - A test for making sure getting the correct (higher) precision + * when it's cross-origin-isolated for performance APIs. + */ + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the forth test case in +// browser_performanceAPI.js. The main difference is this test case verifies +// performance API have more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +let runWorkerTest = async function (data) { + let expectedPrecision = data.precision; + await new Promise(resolve => { + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + let worker = new content.Worker( + "coop_header.sjs?crossOriginIsolated=true&worker=true" + ); + + worker.postMessage({ + type: "runCmdAndGetResult", + cmd: `performance.timeOrigin`, + }); + + const expectedAllEntriesLength = 3; + const expectedResourceEntriesLength = 2; + const expectedTestAndMarkEntriesLength = 1; + + worker.onmessage = function (e) { + if (e.data.type == "result") { + if (e.data.resultOf == "performance.timeOrigin") { + ok( + isRounded(e.data.result, expectedPrecision), + `In a worker, performance.timeOrigin is` + + ` not correctly rounded: ${e.data.result}` + ); + + worker.postMessage({ + type: "runCmds", + cmds: [ + `performance.mark("Test");`, + `performance.mark("Test-End");`, + `performance.measure("Test-Measure", "Test", "Test-End");`, + ], + }); + } else if (e.data.resultOf == "entriesLength") { + is( + e.data.result, + expectedAllEntriesLength, + `In a worker: Incorrect number of ` + + `entries for performance.getEntries() for workers: ` + + `${e.data.result}` + ); + + worker.postMessage({ + type: "getResult", + resultOf: "startTimeAndDuration", + num: 0, + }); + } else if (e.data.resultOf == "startTimeAndDuration") { + let index = e.data.result.index; + let startTime = e.data.result.startTime; + let duration = e.data.result.duration; + ok( + isRounded(startTime, expectedPrecision), + `In a worker, for precision(${expectedPrecision}, ` + + `performance.getEntries(${index}).startTime is not rounded: ` + + `${startTime}` + ); + ok( + isRounded(duration, expectedPrecision), + `In a worker, for precision(${expectedPrecision}), ` + + `performance.getEntries(${index}).duration is not rounded: ` + + `${duration}` + ); + + if (index < 2) { + worker.postMessage({ + type: "getResult", + resultOf: "startTimeAndDuration", + num: index + 1, + }); + } else { + worker.postMessage({ + type: "getResult", + resultOf: "getEntriesByTypeLength", + }); + } + } else if (e.data.resultOf == "entriesByTypeLength") { + is( + e.data.result.markLength, + expectedResourceEntriesLength, + `In a worker: Incorrect number of ` + + `entries for performance.getEntriesByType() for workers: ` + + `${e.data.result.resourceLength}` + ); + is( + e.data.result.testAndMarkLength, + expectedTestAndMarkEntriesLength, + `In a worker: Incorrect number of ` + + `entries for performance.getEntriesByName() for workers: ` + + `${e.data.result.testAndMarkLength}` + ); + + worker.terminate(); + resolve(); + } + } else { + ok(false, `Unknown message type got ${e.data.type}`); + worker.terminate(); + resolve(); + } + }; + }); +}; + +add_task(async function runTestsForWorker() { + // RFP + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 100, + runWorkerTest, + "runTimerTests" + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + }, + 13, + runWorkerTest, + "runTimerTests" + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 0.13, + runWorkerTest, + "runTimerTests" + ); + + // RTP + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + }, + 0.13, + runWorkerTest, + "runTimerTests" + ); + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + crossOriginIsolated: true, + }, + 0.005, + runWorkerTest, + "runTimerTests" + ); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_reduce_time_precision.js b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_reduce_time_precision.js new file mode 100644 index 0000000000..1366604b4c --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_cross_origin_isolated_reduce_time_precision.js @@ -0,0 +1,497 @@ +/** + * Bug 1621677 - A test for making sure getting the correct (higher) precision + * when it's cross-origin-isolated. + */ + +const TEST_SCENARIO_1 = 1; +const TEST_SCENARIO_2 = 2; +const TEST_SCENARIO_3 = 3; +const TEST_SCENARIO_4 = 4; +const TEST_SCENARIO_5 = 5; +const TEST_SCENARIO_6 = 6; +const TEST_SCENARIO_7 = 7; +const TEST_SCENARIO_8 = 8; +const TEST_SCENARIO_9 = 9; +const TEST_SCENARIO_10 = 10; +const TEST_SCENARIO_11 = 11; + +const TEST_SCENARIO_101 = 101; +const TEST_SCENARIO_102 = 102; +const TEST_SCENARIO_103 = 103; +const TEST_SCENARIO_104 = 104; +const TEST_SCENARIO_105 = 105; +const TEST_SCENARIO_106 = 106; +const TEST_SCENARIO_107 = 107; +const TEST_SCENARIO_108 = 108; +const TEST_SCENARIO_109 = 109; +const TEST_SCENARIO_110 = 110; +const TEST_SCENARIO_111 = 111; + +requestLongerTimeout(2); + +let processResultsGlobal = (data, successes, failures) => { + let expectedPrecision = data.precision; + let scenario = data.options.scenario; + let shouldBeRounded = data.options.shouldBeRounded; + for (let success of successes) { + ok( + true, + (shouldBeRounded ? "Should " : "Should not ") + + `have rounded '${success[0]}' to nearest ${expectedPrecision} ms; saw ${success[1]}. ` + + `scenario: TEST_SCENARIO_${scenario}` + ); + } + if (failures.length > 2) { + for (let failure of failures) { + ok( + false, + (shouldBeRounded ? "Should " : "Should not ") + + `have rounded '${failure[0]}' to nearest ${expectedPrecision} ms; saw ${failure[1]}. ` + + `scenario: TEST_SCENARIO_${scenario}` + ); + } + } else if ( + failures.length == 2 && + expectedPrecision < 10 && + failures[0][0].indexOf("Date().getTime()") > 0 && + failures[1][0].indexOf('File([], "").lastModified') > 0 + ) { + /* + * At high precisions, the epoch-based timestamps are large enough that their expected + * rounding values lie directly between two integers; and floating point math is imprecise enough + * that we need to accept these failures + */ + ok( + true, + "Two Free Failures that " + + (data.options.shouldBeRounded ? "ahould " : "should not ") + + `be rounded on the epoch dates and precision: ${expectedPrecision}. ` + + `scenario: TEST_SCENARIO_${data.options.scenario}` + ); + } else if (failures.length == 1) { + ok( + true, + "Free Failure: " + + (data.options.shouldBeRounded ? "Should " : "Should not ") + + `have rounded '${failures[0][0]}' to nearest ${expectedPrecision} ms; saw ${failures[0][1]}. ` + + `scenario: TEST_SCENARIO_${data.options.scenario}` + ); + } +}; + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the test case for window in +// test_reduce_time_precision.html. The main difference is this test case +// verifies DOM API has more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +add_task(async function runRTPTestDOM() { + let runTests = async function (data) { + let expectedPrecision = data.precision; + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + // eslint-disable-next-line + let processResults = eval(data.options.processResultsFunc); + + // Prepare for test of AudioContext.currentTime + // eslint-disable-next-line + let audioContext = new content.AudioContext(); + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + "new content.Date().getTime()", + 'new content.File([], "").lastModified', + "content.performance.now()", + 'new content.Event("").timeStamp', + ]; + // These are measured in seconds, so we need to scale them up + var timeStampCodesDOM = timeStampCodes.concat([ + "audioContext.currentTime * 1000", + ]); + + // If we are not rounding values, this function will invert the return value + let resultSwitchisRounded = function (timeStamp) { + if (timeStamp == 0) { + return true; + } + let result = isRounded(timeStamp, expectedPrecision, content.console); + return data.options.shouldBeRounded ? result : !result; + }; + + // It's possible that even if we shouldn't be rounding something, we get a timestamp that is rounded. + // If we have only one of these outliers, we are okay with that. This should significantly + // reduce intermittents, although it's still likely there will be some intermittents. We can increase + // this if we need to + let successes = [], + failures = []; + + // Loop through each timeStampCode, evaluate it, + // and check if it is rounded + for (let timeStampCode of timeStampCodesDOM) { + // eslint-disable-next-line + let timeStamp = eval(timeStampCode); + + // Audio Contexts increment in intervals of (minimum) 5.4ms, so we don't + // clamp/jitter if the timer precision is les than that. + // (Technically on MBPs they increment in intervals of 2.6 but this is + // non-standard and will eventually be changed. We don't cover this situation + // because we don't really support arbitrary Timer Precision, especially in + // the 2.6 - 5.4ms interval.) + if (timeStampCode.includes("audioContext") && expectedPrecision < 5.4) { + continue; + } + + if (timeStamp != 0 && resultSwitchisRounded(timeStamp)) { + successes = successes.concat([[timeStampCode, timeStamp]]); + } else if (timeStamp != 0) { + failures = failures.concat([[timeStampCode, timeStamp]]); + } + } + + processResults(data, successes, failures); + }; + + // RFP + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_1, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_2, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_3, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runTests + ); + + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_4, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runTests + ); + /* + Disabled because it causes too many intermittents + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_5, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runTests + ); + */ + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_6, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runTests + ); + + // We cannot run the tests with too fine a precision, or it becomes very likely + // to get false results that a number 'should not been rounded', when it really + // wasn't, we had just gotten an accidental match + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_7, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runTests + ); + /* + Disabled because it causes too many intermittents + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_8, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runTests + ); + */ + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_9, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runTests + ); + + // RTP + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + scenario: TEST_SCENARIO_10, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runTests + ); + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_11, + processResultsFunc: processResultsGlobal.toString(), + }, + 0.005, + runTests + ); +}); + +// ================================================================================================ +// ================================================================================================ +// This test case is mostly copy-and-paste from the test case for worker in +// test_reduce_time_precision.html. The main difference is this test case +// verifies DOM API has more precsion when it's in cross-origin-isolated and +// cross-origin-isolated doesn't affect RFP. +let runWorkerTest = async function (data) { + let expectedPrecision = data.precision; + await new Promise(resolve => { + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + // eslint-disable-next-line + let processResults = eval(data.options.processResultsFunc); + + let worker = new content.Worker( + "coop_header.sjs?crossOriginIsolated=true&worker=true" + ); + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + "new Date().getTime()", + 'new File([], "").lastModified', + "performance.now()", + 'new Event("").timeStamp', + ]; + + let promises = [], + successes = [], + failures = []; + for (let timeStampCode of timeStampCodes) { + promises.push( + new Promise(res => { + worker.postMessage({ + type: "runCmdAndGetResult", + cmd: timeStampCode, + }); + + worker.addEventListener("message", function (e) { + // If we are not rounding values, this function will invert the return value + let resultSwitchisRounded = function (timeStamp) { + if (timeStamp == 0) { + return true; + } + let result = isRounded(timeStamp, expectedPrecision); + return data.options.shouldBeRounded ? result : !result; + }; + + if (e.data.type == "result") { + if (e.data.resultOf == timeStampCode) { + if (resultSwitchisRounded(e.data.result)) { + successes = successes.concat([ + [timeStampCode, e.data.result], + ]); + } else { + failures = failures.concat([[timeStampCode, e.data.result]]); + } + worker.removeEventListener("message", this); + res(); + } + + return; + } + + ok(false, `Unknown message type. Got ${e.data.type}`); + res(); + }); + }) + ); + } + + Promise.all(promises).then(_ => { + worker.terminate(); + processResults(data, successes, failures); + resolve(); + }); + }); +}; + +add_task(async function runRTPTestsForWorker() { + // RFP + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_101, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runWorkerTest + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_102, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runWorkerTest + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_103, + processResultsFunc: processResultsGlobal.toString(), + }, + 100, + runWorkerTest + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_104, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runWorkerTest + ); + /* + Disabled because it causes too many intermittents + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_105, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runWorkerTest + ); + */ + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_106, + processResultsFunc: processResultsGlobal.toString(), + }, + 13, + runWorkerTest + ); + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprinting: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_107, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runWorkerTest + ); + /* Disabled because it causes too many intermittents + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + crossOriginIsolated: true, + shouldBeRounded: false, + scenario: TEST_SCENARIO_108, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runWorkerTest + ); + */ + await setupAndRunCrossOriginIsolatedTest( + { + resistFingerprintingPBMOnly: true, + openPrivateWindow: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_109, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runWorkerTest + ); + + // RTP + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + scenario: TEST_SCENARIO_110, + processResultsFunc: processResultsGlobal.toString(), + }, + 7.97, + runWorkerTest + ); + await setupAndRunCrossOriginIsolatedTest( + { + reduceTimerPrecision: true, + crossOriginIsolated: true, + scenario: TEST_SCENARIO_111, + processResultsFunc: processResultsGlobal.toString(), + }, + 0.005, + runWorkerTest + ); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js new file mode 100644 index 0000000000..13dcec8ea1 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js @@ -0,0 +1,397 @@ +/* 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/. + * + * Bug 1407366 - A test case for reassuring the size of the content viewport is + * rounded if the window is resized when letterboxing is enabled. + * + * A helpful note: if this test starts randomly failing; it may be because the + * zoom level was not reset by an earlier-run test. See Bug 1407366 for an + * example. + */ + +const { RFPHelper } = ChromeUtils.importESModule( + "resource://gre/modules/RFPHelper.sys.mjs" +); + +// A set of test cases which defines the width and the height of the outer window. +const TEST_CASES = [ + { width: 1250, height: 1000 }, + { width: 1500, height: 1050 }, + { width: 1120, height: 760 }, + { width: 800, height: 600 }, + { width: 640, height: 400 }, + { width: 500, height: 350 }, +]; + +function getPlatform() { + const { OS } = Services.appinfo; + if (OS == "WINNT") { + return "win"; + } else if (OS == "Darwin") { + return "mac"; + } + return "linux"; +} + +function handleOSFuzziness(aContent, aTarget) { + /* + * On Windows, we observed off-by-one pixel differences that + * couldn't be expained. When manually setting the window size + * to try to reproduce it; it did not occur. + */ + if (getPlatform() == "win") { + return Math.abs(aContent - aTarget) <= 1; + } + return aContent == aTarget; +} + +function checkForDefaultSetting( + aContentWidth, + aContentHeight, + aRealWidth, + aRealHeight +) { + // We can get the rounded size by subtracting twice the margin. + let targetWidth = aRealWidth - 2 * RFPHelper.steppedRange(aRealWidth); + let targetHeight = aRealHeight - 2 * RFPHelper.steppedRange(aRealHeight); + + // This platform-specific code is explained in the large comment below. + if (getPlatform() != "linux") { + ok( + handleOSFuzziness(aContentWidth, targetWidth), + `Default Dimensions: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetWidth}px` + ); + + ok( + handleOSFuzziness(aContentHeight, targetHeight), + `Default Dimensions: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetHeight}px` + ); + + // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends + return true; + } + // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds. + return ( + handleOSFuzziness(aContentWidth, targetWidth) && + handleOSFuzziness(aContentHeight, targetHeight) + ); +} + +async function test_dynamical_window_rounding(aWindow, aCheckFunc) { + // We need to wait for the updating the margins for the newly opened tab, or + // it will affect the following tests. + let promiseForTheFirstRounding = TestUtils.topicObserved( + "test:letterboxing:update-margin-finish" + ); + + info("Open a content tab for testing."); + let tab = await BrowserTestUtils.openNewForegroundTab( + aWindow.gBrowser, + TEST_PATH + "file_dummy.html" + ); + + info("Wait until the margins are applied for the opened tab."); + await promiseForTheFirstRounding; + + let getContainerSize = aTab => { + let browserContainer = aWindow.gBrowser.getBrowserContainer( + aTab.linkedBrowser + ); + return { + containerWidth: browserContainer.clientWidth, + containerHeight: browserContainer.clientHeight, + }; + }; + + for (let { width, height } of TEST_CASES) { + let caseString = "Case " + width + "x" + height + ": "; + // Create a promise for waiting for the margin update. + let promiseRounding = TestUtils.topicObserved( + "test:letterboxing:update-margin-finish" + ); + + let { containerWidth, containerHeight } = getContainerSize(tab); + + info( + caseString + + "Resize the window and wait until resize event happened (currently " + + containerWidth + + "x" + + containerHeight + + ")" + ); + await new Promise(resolve => { + ({ containerWidth, containerHeight } = getContainerSize(tab)); + info( + caseString + + "Resizing (currently " + + containerWidth + + "x" + + containerHeight + + ")" + ); + + aWindow.onresize = () => { + ({ containerWidth, containerHeight } = getContainerSize(tab)); + info( + caseString + + "Resized (currently " + + containerWidth + + "x" + + containerHeight + + ")" + ); + if (getPlatform() == "linux" && containerWidth != width) { + /* + * We observed frequent test failures that resulted from receiving an onresize + * event where the browser was resized to an earlier requested dimension. This + * resize event happens on Linux only, and is an artifact of the asynchronous + * resizing. (See more discussion on 1407366#53) + * + * We cope with this problem in two ways. + * + * 1: If we detect that the browser was resized to the wrong value; we + * redo the resize. (This is the lines of code immediately following this + * comment) + * 2: We repeat the test until it works using waitForCondition(). But we still + * test Win/Mac more thoroughly: they do not loop in waitForCondition more + * than once, and can fail the test on the first attempt (because their + * check() functions use ok() while on Linux, we do not all ok() and instead + * rely on waitForCondition to fail). + * + * The logging statements in this test, and RFPHelper.jsm, help narrow down and + * illustrate the issue. + */ + info(caseString + "We hit the weird resize bug. Resize it again."); + aWindow.resizeTo(width, height); + } else { + resolve(); + } + }; + aWindow.resizeTo(width, height); + }); + + ({ containerWidth, containerHeight } = getContainerSize(tab)); + info( + caseString + + "Waiting until margin has been updated on browser element. (currently " + + containerWidth + + "x" + + containerHeight + + ")" + ); + await promiseRounding; + + info(caseString + "Get innerWidth/Height from the content."); + await BrowserTestUtils.waitForCondition(async () => { + let { contentWidth, contentHeight } = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return { + contentWidth: content.innerWidth, + contentHeight: content.innerHeight, + }; + } + ); + + info(caseString + "Check the result."); + return aCheckFunc( + contentWidth, + contentHeight, + containerWidth, + containerHeight + ); + }, "Default Dimensions: The content window width is correctly rounded into."); + } + + BrowserTestUtils.removeTab(tab); +} + +async function test_customize_width_and_height(aWindow) { + const test_dimensions = `120x80, 200x143, 335x255, 600x312, 742x447, 813x558, + 990x672, 1200x733, 1470x858`; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.letterboxing.dimensions", test_dimensions], + ], + }); + + let dimensions_set = test_dimensions.split(",").map(item => { + let sizes = item.split("x").map(size => parseInt(size, 10)); + + return { + width: sizes[0], + height: sizes[1], + }; + }); + + let checkDimension = ( + aContentWidth, + aContentHeight, + aRealWidth, + aRealHeight + ) => { + let matchingArea = aRealWidth * aRealHeight; + let minWaste = Number.MAX_SAFE_INTEGER; + let targetDimensions = undefined; + + // Find the dimensions which waste the least content area. + for (let dim of dimensions_set) { + if (dim.width > aRealWidth || dim.height > aRealHeight) { + continue; + } + + let waste = matchingArea - dim.width * dim.height; + + if (waste >= 0 && waste < minWaste) { + targetDimensions = dim; + minWaste = waste; + } + } + + // This platform-specific code is explained in the large comment above. + if (getPlatform() != "linux") { + ok( + handleOSFuzziness(aContentWidth, targetDimensions.width), + `Custom Dimension: The content window width is correctly rounded into. ${aRealWidth}px -> ${aContentWidth}px should equal ${targetDimensions.width}` + ); + + ok( + handleOSFuzziness(aContentHeight, targetDimensions.height), + `Custom Dimension: The content window height is correctly rounded into. ${aRealHeight}px -> ${aContentHeight}px should equal ${targetDimensions.height}` + ); + + // Using ok() above will cause Win/Mac to fail on even the first test, we don't need to repeat it, return true so waitForCondition ends + return true; + } + // Returning true or false depending on if the test succeeded will cause Linux to repeat until it succeeds. + return ( + handleOSFuzziness(aContentWidth, targetDimensions.width) && + handleOSFuzziness(aContentHeight, targetDimensions.height) + ); + }; + + await test_dynamical_window_rounding(aWindow, checkDimension); + + await SpecialPowers.popPrefEnv(); +} + +async function test_no_rounding_for_chrome(aWindow) { + // First, resize the window to a size which is not rounded. + await new Promise(resolve => { + aWindow.onresize = () => resolve(); + aWindow.resizeTo(700, 450); + }); + + // open a chrome privilege tab, like about:config. + let tab = await BrowserTestUtils.openNewForegroundTab( + aWindow.gBrowser, + "about:config" + ); + + // Check that the browser element should not have a margin. + is( + tab.linkedBrowser.style.margin, + "", + "There is no margin around chrome tab." + ); + + BrowserTestUtils.removeTab(tab); +} + +// Tests that the findbar opening and closing causes a margin update. +async function test_findbar(aWindow) { + // First, resize the window to a size which is not rounded. + await new Promise(resolve => { + aWindow.onresize = () => resolve(); + aWindow.resizeTo(701, 451); + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + aWindow.gBrowser, + TEST_PATH + "file_dummy.html" + ); + + let promiseRounding = TestUtils.topicObserved( + "test:letterboxing:update-margin-finish" + ); + + let findBarOpenPromise = BrowserTestUtils.waitForEvent( + aWindow, + "findbaropen" + ); + EventUtils.synthesizeKey("F", { accelKey: true }, aWindow); + await findBarOpenPromise; + await promiseRounding; + + ok(true, "Margin updated when findbar opened"); + + promiseRounding = TestUtils.topicObserved( + "test:letterboxing:update-margin-finish" + ); + + let findBarClosePromise = BrowserTestUtils.waitForEvent( + aWindow, + "findbarclose" + ); + EventUtils.synthesizeKey("KEY_Escape", {}, aWindow); + await findBarClosePromise; + await promiseRounding; + + ok(true, "Margin updated when findbar closed"); + + BrowserTestUtils.removeTab(tab); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.letterboxing", true], + ["privacy.resistFingerprinting.letterboxing.testing", true], + ], + }); +}); + +add_task(async function do_tests() { + // Store the original window size before testing. + let originalOuterWidth = window.outerWidth; + let originalOuterHeight = window.outerHeight; + + info("Run test for the default window rounding."); + await test_dynamical_window_rounding(window, checkForDefaultSetting); + + info("Run test for the window rounding with customized dimensions."); + await test_customize_width_and_height(window); + + info("Run test for no margin around tab with the chrome privilege."); + await test_no_rounding_for_chrome(window); + + await test_findbar(window); + + // Restore the original window size. + window.outerWidth = originalOuterWidth; + window.outerHeight = originalOuterHeight; + + // Testing that whether the dynamical rounding works for new windows. + let win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Run test for the default window rounding in new window."); + await test_dynamical_window_rounding(win, checkForDefaultSetting); + + info( + "Run test for the window rounding with customized dimensions in new window." + ); + await test_customize_width_and_height(win); + + info( + "Run test for no margin around tab with the chrome privilege in new window." + ); + await test_no_rounding_for_chrome(win); + + await test_findbar(win); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js new file mode 100644 index 0000000000..47914e098e --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js @@ -0,0 +1,126 @@ +/** + * This test only tests values in the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely in normal and PBM + * - FPP is enabled entirely in normal and PBM + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html?mode=iframe`; + +requestLongerTimeout(2); + +let extraData = { + etp_reload: true, +}; + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task( + defaultsTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +// The ETP toggle _does not_ override RFP, only FPP. +expectedResults = structuredClone(allSpoofed); +add_task( + simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simplePBMRFPTest.bind( + null, + uri, + testHWConcurrency, + expectedResults, + extraData + ) +); + +expectedResults = structuredClone(allNotSpoofed); +add_task( + simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allNotSpoofed); +add_task( + simplePBMFPPTest.bind( + null, + uri, + testHWConcurrency, + expectedResults, + extraData + ) +); + +// These test cases exercise the unusual configuration of RFP enabled in PBM and FPP enabled +// in normal browsing mode. +let extraPrefs = [ + ["privacy.resistFingerprinting.pbmode", true], + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "+HardwareConcurrency"], +]; + +let this_extraData = structuredClone(extraData); +this_extraData.testDesc = + "RFP enabled in PBM, FPP enabled globally, testing in normal browsing mode"; +expectedResults = allNotSpoofed; +add_task( + defaultsTest.bind( + null, + uri, + testHWConcurrency, + structuredClone(expectedResults), + this_extraData, + structuredClone(extraPrefs) + ) +); + +this_extraData = structuredClone(extraData); +this_extraData.testDesc = + "RFP enabled in PBM, FPP enabled globally, testing in PBM"; +this_extraData.private_window = true; +expectedResults = allSpoofed; +add_task( + defaultsTest.bind( + null, + uri, + testHWConcurrency, + structuredClone(expectedResults), + this_extraData, + structuredClone(extraPrefs) + ) +); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js new file mode 100644 index 0000000000..ec69d436c8 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html?mode=iframe`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js new file mode 100644 index 0000000000..fd76d52da1 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in an about:blank document that is created by the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js new file mode 100644 index 0000000000..62c2363cd2 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in an about:srcdoc document that is created by the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js new file mode 100644 index 0000000000..6ab0be2b9a --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in a blob document that is created by the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js new file mode 100644 index 0000000000..6a643000bd --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js @@ -0,0 +1,110 @@ +/** + * This test only tests values in a blob document that is created by the iframe on one domain, then passed + * to a cross-origin domain to embed. + * It is a bit different from all the other tests in this series, because instead of the framer doing nothing + * except frame the framee; the framer creates the blob document, and the framee embeds the blob document. + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +// In theory this should be Not Spoofed, however, in this test there is a blob: document that +// has a content principal and a reference to the iframe's parent (when Fission is disabled anyway.) +// The blob's principal does not match the parent's principal, so it is up to the blob to determine +// if it should resist fingerprinting on its own. +// It decides _not_ to resist fingerprinting, but only because nsContentUtils::IsURIInList has +// a check `if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) { return false; }` +// We could in theory, modify that check to check the blob's creation uri, and that would work. +// But I am nervous about changing that code. +// expectedResults = structuredClone(allNotSpoofed); +expectedResults = structuredClone(allSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +// Same as A above. +//expectedResults = structuredClone(allNotSpoofed); +expectedResults = structuredClone(allSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js new file mode 100644 index 0000000000..72d5ec9ba0 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in a data document that is created by the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js new file mode 100644 index 0000000000..684ae7ba5a --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js @@ -0,0 +1,96 @@ +/** + * This test only tests values in a sandboxed iframe that is created by a parent iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testD.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js new file mode 100644 index 0000000000..13e7147236 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js @@ -0,0 +1,76 @@ +/** + * This test tests values in a popup, it does not test them on the page that made the popup + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the maker and popup + * - (C) RFP is exempted on the maker but not the popup + * - (E) RFP is not exempted on the maker nor the popup + * - (G) RFP is not exempted on the maker but is on the popup + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html?mode=popup`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the maker and popup +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (C) RFP is exempted on the maker but not the popup +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the maker nor the popup +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); + +// (G) RFP is not exempted on the maker but is on the popup +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js new file mode 100644 index 0000000000..982f256e76 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js @@ -0,0 +1,67 @@ +/** + * This test only tests values in an about:blank document that opened in a popup + * Because there is no interaction with a third party domain, there's a lot fewer tests + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the popup maker + * - (E) RFP is not exempted on the popup maker + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the popup maker +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the popup maker +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js new file mode 100644 index 0000000000..7a81b9980c --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js @@ -0,0 +1,67 @@ +/** + * This test only tests values in a blob document that is opened in a popup + * Because there is no interaction with a third party domain, there's a lot fewer tests + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the popup maker + * - (E) RFP is not exempted on the popup maker + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the popup maker +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the popup maker +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js new file mode 100644 index 0000000000..a8046e00d1 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js @@ -0,0 +1,81 @@ +/** + * This test only tests values in a blob document that is opened in a popup + * Because there is no interaction with a third party domain, there's a lot fewer tests + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the popup maker + * - (E) RFP is not exempted on the popup maker + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html?submode=noopener`; +const await_uri = loadedURL => loadedURL.startsWith("blob:"); + +requestLongerTimeout(2); + +let extraData = { + noopener: true, + await_uri, +}; + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task( + defaultsTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +// (A) RFP is exempted on the popup maker +// Ordinarily, RFP would be exempted, however because the opener relationship is severed +// there is nothing to grant it an exemption, so it is not exempted. +expectedResults = structuredClone(allSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults, extraData)); + +// (E) RFP is not exempted on the popup maker +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults, extraData)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js new file mode 100644 index 0000000000..175d4c57af --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js @@ -0,0 +1,67 @@ +/** + * This test only tests values in a data document that is opened in a popup + * Because there is no interaction with a third party domain, there's a lot fewer tests + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the popup maker + * - (E) RFP is not exempted on the popup maker + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults)); + +// (A) RFP is exempted on the popup maker +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults)); + +// (E) RFP is not exempted on the popup maker +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js new file mode 100644 index 0000000000..99d1a82937 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js @@ -0,0 +1,81 @@ +/** + * This test only tests values in a data document that is opened in a popup with noopener + * Because there is no interaction with a third party domain, there's a lot fewer tests + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + + * + * - (A) RFP is exempted on the popup maker + * - (E) RFP is not exempted on the popup maker + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html?submode=noopener`; +const await_uri = loadedURL => loadedURL.startsWith("data:"); + +requestLongerTimeout(2); + +let extraData = { + noopener: true, + await_uri, +}; + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task( + defaultsTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +// (A) RFP is exempted on the popup maker +// Ordinarily, RFP would be exempted, however because the opener relationship is severed +// there is nothing to grant it an exemption, so it is not exempted. +expectedResults = structuredClone(allSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults, extraData)); + +// (E) RFP is not exempted on the popup maker +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults, extraData)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js new file mode 100644 index 0000000000..f27c625356 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js @@ -0,0 +1,89 @@ +/** + * This test tests values in a popup that is opened with noopener, it does not test them on the page that made the popup + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * - FPP is enabled entirely + * + * - (A) RFP is exempted on the maker and popup + * - (C) RFP is exempted on the maker but not the popup + * - (E) RFP is not exempted on the maker nor the popup + * - (G) RFP is not exempted on the maker but is on the popup + * + */ + +"use strict"; + +const SPOOFED_HW_CONCURRENCY = 2; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testHWConcurrency(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); +} + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + hardwareConcurrency: DEFAULT_HARDWARE_CONCURRENCY, +}; +const allSpoofed = { + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html?mode=popup&submode=noopener`; +const await_uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html?mode=popup`; + +requestLongerTimeout(2); + +let extraData = { + noopener: true, + await_uri, +}; + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task( + defaultsTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +expectedResults = structuredClone(allSpoofed); +add_task( + simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData) +); + +// (A) RFP is exempted on the maker and popup +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testHWConcurrency, expectedResults, extraData)); + +// (C) RFP is exempted on the maker but not the popup +expectedResults = structuredClone(allSpoofed); +add_task(testC.bind(null, uri, testHWConcurrency, expectedResults, extraData)); + +// (E) RFP is not exempted on the maker nor the popup +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testHWConcurrency, expectedResults, extraData)); + +// (G) RFP is not exempted on the maker but is on the popup +// Ordinarily, RFP would not be exempted, however because the opener relationship is severed +// it is safe to exempt the popup +expectedResults = structuredClone(allNotSpoofed); +add_task(testG.bind(null, uri, testHWConcurrency, expectedResults, extraData)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_math.js b/browser/components/resistfingerprinting/test/browser/browser_math.js new file mode 100644 index 0000000000..1d10ad9888 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_math.js @@ -0,0 +1,115 @@ +/** + * Bug 531915 - A test for verifying that the JS Math fingerprint is constant + * when using fdlibm for Math.sin, Math.cos, and Math.tan. + */ + +async function test_math(rfp_pref, fdlibm_pref) { + info( + `privacy.resistFingerprinting: ${rfp_pref} javascript.options.use_fdlibm_for_sin_cos_tan: ${fdlibm_pref}` + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["javascript.options.use_fdlibm_for_sin_cos_tan", fdlibm_pref], + ["privacy.resistFingerprinting", rfp_pref], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_dummy.html" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + // prettier-ignore + function test() { + // + // Tests adapted from https://github.com/arkenfox/TZP/blob/5e3f5ff2c64b4edc7beecd8308aa4f7a3efb49e3/tests/math.html#L158-L319 + // + is(Math.cos(1e251), -0.37419577499634155, "Math.cos(1e251)"); + is(Math.cos(1e140), -0.7854805190645291, "Math.cos(1e140)"); + is(Math.cos(1e12), 0.7914463018528902, "Math.cos(1e12)"); + is(Math.cos(1e130), -0.767224894221913, "Math.cos(1e130)"); + is(Math.cos(1e272), -0.7415825695514536, "Math.cos(1e272)"); + is(Math.cos(1), 0.5403023058681398, "Math.cos(1)"); + is(Math.cos(1e284), 0.7086865671674247, "Math.cos(1e284)"); + is(Math.cos(1e75), -0.7482651726250321, "Math.cos(1e75)"); + is(Math.cos(Math.PI), -1, "Math.cos(Math.PI)"); + is(Math.cos(-1e308), -0.8913089376870335, "Math.cos(-1e308)"); + is(Math.cos(13 * Math.E), -0.7108118501064332, "Math.cos(13 * Math.E)"); + is(Math.cos(57 * Math.E), -0.536911695749024, "Math.cos(57 * Math.E)"); + is(Math.cos(21 * Math.LN2), -0.4067775970251724, "Math.cos(21 * Math.LN2)"); + is(Math.cos(51 * Math.LN2), -0.7017203400855446, "Math.cos(51 * Math.LN2)"); + is(Math.cos(21 * Math.LOG2E), 0.4362848063618998, "Math.cos(21 * Math.LOG2E)"); + is(Math.cos(25 * Math.SQRT2), -0.6982689820462377, "Math.cos(25 * Math.SQRT2)"); + is(Math.cos(50 * Math.SQRT1_2), -0.6982689820462377, "Math.cos(50 * Math.SQRT1_2)"); + is(Math.cos(21 * Math.SQRT1_2), -0.6534063185820198, "Math.cos(21 * Math.SQRT1_2)"); + is(Math.cos(17 * Math.LOG10E), 0.4537557425982784, "Math.cos(17 * Math.LOG10E)"); + is(Math.cos(2 * Math.LOG10E), 0.6459044007438142, "Math.cos(2 * Math.LOG10E)"); + + is(Math.sin(1e251), -0.9273497301314576, "Math.sin(1e251)"); + is(Math.sin(1e140), -0.6188863822787813, "Math.sin(1e140)"); + is(Math.sin(1e12), -0.6112387023768895, "Math.sin(1e12)"); + is(Math.sin(1e130), 0.6413781736901984, "Math.sin(1e130)"); + is(Math.sin(1e272), 0.6708616046081811, "Math.sin(1e272)"); + is(Math.sin(1), 0.8414709848078965, "Math.sin(1)"); + is(Math.sin(1e284), -0.7055234578073583, "Math.sin(1e284)"); + is(Math.sin(1e75), 0.66339975236386, "Math.sin(1e75)"); + is(Math.sin(Math.PI), 1.2246467991473532e-16, "Math.sin(Math.PI)"); + is(Math.sin(39 * Math.E), -0.7181630308570678, "Math.sin(39 * Math.E)"); + is(Math.sin(35 * Math.LN2), -0.765996413898051, "Math.sin(35 * Math.LN2)"); + is(Math.sin(110 * Math.LOG2E), 0.9989410140273757, "Math.sin(110 * Math.LOG2E)"); + is(Math.sin(7 * Math.LOG10E), 0.10135692924965616, "Math.sin(7 * Math.LOG10E)"); + is(Math.sin(35 * Math.SQRT1_2), -0.3746357547858202, "Math.sin(35 * Math.SQRT1_2)"); + is(Math.sin(21 * Math.SQRT2), -0.9892668187780497, "Math.sin(21 * Math.SQRT2)"); + + is(Math.tan(1e251), 2.478247463217681, "Math.tan(1e251)"); + is(Math.tan(1e140), 0.7879079967710036, "Math.tan(1e140)"); + is(Math.tan(1e12), -0.7723059681318761, "Math.tan(1e12)"); + is(Math.tan(1e130), -0.8359715365344825, "Math.tan(1e130)"); + is(Math.tan(1e272), -0.904635076595654, "Math.tan(1e272)"); + is(Math.tan(1), 1.5574077246549023, "Math.tan(1)"); + is(Math.tan(1e284), -0.9955366596368418, "Math.tan(1e284)"); + is(Math.tan(1e75), -0.8865837628611647, "Math.tan(1e75)"); + is(Math.tan(-1e308), 0.5086861259107568, "Math.tan(-1e308)"); + is(Math.tan(Math.PI), -1.2246467991473532e-16, "Math.tan(Math.PI)"); + is(Math.tan(6 * Math.E), 0.6866761546452431, "Math.tan(6 * Math.E)"); + is(Math.tan(6 * Math.LN2), 1.6182817135715877, "Math.tan(6 * Math.LN2)"); + is(Math.tan(10 * Math.LOG2E), -3.3537128705376014, "Math.tan(10 * Math.LOG2E)"); + is(Math.tan(17 * Math.SQRT2), -1.9222955461799982, "Math.tan(17 * Math.SQRT2)"); + is(Math.tan(34 * Math.SQRT1_2), -1.9222955461799982, "Math.tan(34 * Math.SQRT1_2)"); + is(Math.tan(10 * Math.LOG10E), 2.5824856130712432, "Math.tan(10 * Math.LOG10E)"); + + // + // Tests adapted from https://github.com/fingerprintjs/fingerprintjs/blob/7096a5589af495f1f46067963e13ad27d887d185/src/sources/math.ts#L32-L64 + // + is(Math.acos(0.123124234234234242), 1.4473588658278522, "Math.acos(0.123124234234234242)"); + is(Math.acosh(1e308), 709.889355822726, "Math.acosh(1e308)"); + is(Math.asin(0.123124234234234242), 0.12343746096704435, "Math.asin(0.123124234234234242)"); + is(Math.asinh(1), 0.881373587019543, "Math.asinh(1)"); + is(Math.atanh(0.5), 0.5493061443340548, "Math.atanh(0.5)"); + is(Math.atan(0.5), 0.4636476090008061, "Math.atan(0.5)"); + is(Math.sin(-1e300), 0.8178819121159085, "Math.sin(-1e300)"); + is(Math.sinh(1), 1.1752011936438014, "Math.sinh(1)"); + is(Math.cos(10.000000000123), -0.8390715290095377, "Math.cos(10.000000000123)"); + is(Math.cosh(1), 1.5430806348152437, "Math.cosh(1)"); + is(Math.tan(-1e300), -1.4214488238747245, "Math.tan(-1e300)"); + is(Math.tanh(1), 0.7615941559557649, "Math.tanh(1)"); + is(Math.exp(1), 2.718281828459045, "Math.exp(1)"); + is(Math.expm1(1), 1.718281828459045, "Math.expm1(1)"); + is(Math.log1p(10), 2.3978952727983707, "Math.log1p(10)"); + } + + // Run test in the context of the page. + Cu.exportFunction(is, content, { defineAs: "is" }); + content.eval(`(${test})()`); + }); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +} + +add_task(test_math.bind(null, false, true)); +add_task(test_math.bind(null, true, false)); +add_task(test_math.bind(null, true, true)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_navigator.js b/browser/components/resistfingerprinting/test/browser/browser_navigator.js new file mode 100644 index 0000000000..9cb0aa9e8d --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_navigator.js @@ -0,0 +1,465 @@ +/** + * Bug 1333651 - A test case for making sure the navigator object has been + * spoofed/disabled correctly. + */ + +"use strict"; + +const CC = Components.Constructor; + +ChromeUtils.defineESModuleGetters(this, { + WindowsVersionInfo: + "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs", +}); + +let expectedResults; + +let osVersion = Services.sysinfo.get("version"); +if (AppConstants.platform == "macosx") { + // Convert Darwin version to macOS version: 19.x.x -> 10.15 etc. + // https://en.wikipedia.org/wiki/Darwin_%28operating_system%29 + let DarwinVersionParts = osVersion.split("."); + let DarwinMajorVersion = +DarwinVersionParts[0]; + let macOsMinorVersion = DarwinMajorVersion - 4; + if (macOsMinorVersion > 15) { + macOsMinorVersion = 15; + } + osVersion = `10.${macOsMinorVersion}`; +} + +const DEFAULT_APPVERSION = { + linux: "5.0 (X11)", + win: "5.0 (Windows)", + macosx: "5.0 (Macintosh)", + android: `5.0 (Android ${osVersion})`, + other: "5.0 (X11)", +}; + +const SPOOFED_APPVERSION = { + linux: "5.0 (X11)", + win: "5.0 (Windows)", + macosx: "5.0 (Macintosh)", + android: "5.0 (Android 10)", + other: "5.0 (X11)", +}; + +let cpuArch = Services.sysinfo.get("arch"); +if (cpuArch == "x86-64") { + // Convert CPU arch "x86-64" to "x86_64" used in Linux and Android UAs. + cpuArch = "x86_64"; +} + +const DEFAULT_PLATFORM = { + linux: `Linux ${cpuArch}`, + win: "Win32", + macosx: "MacIntel", + android: `Linux ${cpuArch}`, + other: `Linux ${cpuArch}`, +}; + +const SPOOFED_PLATFORM = { + linux: "Linux x86_64", + win: "Win32", + macosx: "MacIntel", + android: "Linux aarch64", + other: "Linux x86_64", +}; + +// If comparison with the WindowsOscpu value fails in the future, it's time to +// evaluate if exposing a new Windows version to the Web is appropriate. See +// https://bugzilla.mozilla.org/show_bug.cgi?id=1693295 +let WindowsOscpu = null; +if (AppConstants.platform == "win") { + let isWin11 = WindowsVersionInfo.get().buildNumber >= 22000; + WindowsOscpu = + cpuArch == "x86_64" || (cpuArch == "aarch64" && isWin11) + ? `Windows NT ${osVersion}; Win64; x64` + : `Windows NT ${osVersion}`; +} + +const DEFAULT_OSCPU = { + linux: `Linux ${cpuArch}`, + win: WindowsOscpu, + macosx: `Intel Mac OS X ${osVersion}`, + android: `Linux ${cpuArch}`, + other: `Linux ${cpuArch}`, +}; + +const SPOOFED_OSCPU = { + linux: "Linux x86_64", + win: "Windows NT 10.0; Win64; x64", + macosx: "Intel Mac OS X 10.15", + android: "Linux aarch64", + other: "Linux x86_64", +}; + +const DEFAULT_UA_OS = { + linux: `X11; Linux ${cpuArch}`, + win: WindowsOscpu, + macosx: `Macintosh; Intel Mac OS X ${osVersion}`, + android: `Android ${osVersion}; Mobile`, + other: `X11; Linux ${cpuArch}`, +}; + +const SPOOFED_UA_NAVIGATOR_OS = { + linux: "X11; Linux x86_64", + win: "Windows NT 10.0; Win64; x64", + macosx: "Macintosh; Intel Mac OS X 10.15", + android: "Android 10; Mobile", + other: "X11; Linux x86_64", +}; +const SPOOFED_UA_HTTPHEADER_OS = { + linux: "Windows NT 10.0", + win: "Windows NT 10.0", + macosx: "Windows NT 10.0", + android: "Android 10; Mobile", + other: "Windows NT 10.0", +}; +const SPOOFED_HW_CONCURRENCY = 2; + +const CONST_APPCODENAME = "Mozilla"; +const CONST_APPNAME = "Netscape"; +const CONST_PRODUCT = "Gecko"; +const CONST_PRODUCTSUB = "20100101"; +const CONST_VENDOR = ""; +const CONST_VENDORSUB = ""; + +const appVersion = parseInt(Services.appinfo.version); +const rvVersion = + parseInt( + Services.prefs.getIntPref("network.http.useragent.forceRVOnly", 0), + 0 + ) || appVersion; +const spoofedVersion = AppConstants.platform == "android" ? "115" : appVersion; + +const LEGACY_UA_GECKO_TRAIL = "20100101"; + +const DEFAULT_UA_GECKO_TRAIL = { + linux: LEGACY_UA_GECKO_TRAIL, + win: LEGACY_UA_GECKO_TRAIL, + macosx: LEGACY_UA_GECKO_TRAIL, + android: `${appVersion}.0`, + other: LEGACY_UA_GECKO_TRAIL, +}; + +const SPOOFED_UA_GECKO_TRAIL = { + linux: LEGACY_UA_GECKO_TRAIL, + win: LEGACY_UA_GECKO_TRAIL, + macosx: LEGACY_UA_GECKO_TRAIL, + android: `${spoofedVersion}.0`, + other: LEGACY_UA_GECKO_TRAIL, +}; + +async function testUserAgentHeader() { + const BASE = + "http://mochi.test:8888/browser/browser/components/resistfingerprinting/test/browser/"; + const TEST_TARGET_URL = `${BASE}file_navigator_header.sjs?`; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TARGET_URL + ); + + let result = await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + return content.document.body.textContent; + }); + + is( + result, + expectedResults.userAgentHeader, + `Checking ${expectedResults.testDesc} User Agent HTTP Header.` + ); + + BrowserTestUtils.removeTab(tab); +} + +async function testNavigator() { + // Open a tab to collect result. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_navigator.html" + ); + + let result = await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + return content.document.getElementById("result").innerHTML; + }); + + result = JSON.parse(result); + + let testDesc = expectedResults.testDesc; + + is( + result.appVersion, + expectedResults.appVersion, + `Checking ${testDesc} navigator.appVersion.` + ); + is( + result.platform, + expectedResults.platform, + `Checking ${testDesc} navigator.platform.` + ); + is( + result.userAgent, + expectedResults.userAgentNavigator, + `Checking ${testDesc} navigator.userAgent.` + ); + is( + result.mimeTypesLength, + expectedResults.mimeTypesLength, + `Navigator.mimeTypes has a length of ${expectedResults.mimeTypesLength}.` + ); + is( + result.pluginsLength, + expectedResults.pluginsLength, + `Navigator.plugins has a length of ${expectedResults.pluginsLength}.` + ); + is( + result.oscpu, + expectedResults.oscpu, + `Checking ${testDesc} navigator.oscpu.` + ); + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); + + is( + result.appCodeName, + CONST_APPCODENAME, + "Navigator.appCodeName reports correct constant value." + ); + is( + result.appName, + CONST_APPNAME, + "Navigator.appName reports correct constant value." + ); + is( + result.product, + CONST_PRODUCT, + "Navigator.product reports correct constant value." + ); + is( + result.productSub, + CONST_PRODUCTSUB, + "Navigator.productSub reports correct constant value." + ); + is( + result.vendor, + CONST_VENDOR, + "Navigator.vendor reports correct constant value." + ); + is( + result.vendorSub, + CONST_VENDORSUB, + "Navigator.vendorSub reports correct constant value." + ); + + BrowserTestUtils.removeTab(tab); +} + +async function testWorkerNavigator() { + // Open a tab to collect result from worker. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_dummy.html" + ); + + let result = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + let worker = new content.SharedWorker( + "file_navigatorWorker.js", + "WorkerNavigatorTest" + ); + + let res = await new Promise(resolve => { + worker.port.onmessage = function (e) { + resolve(e.data); + }; + }); + + return res; + } + ); + + result = JSON.parse(result); + + let testDesc = expectedResults.testDesc; + + is( + result.appVersion, + expectedResults.appVersion, + `Checking ${testDesc} worker navigator.appVersion.` + ); + is( + result.platform, + expectedResults.platform, + `Checking ${testDesc} worker navigator.platform.` + ); + is( + result.userAgent, + expectedResults.userAgentNavigator, + `Checking ${testDesc} worker navigator.userAgent.` + ); + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} worker navigator.hardwareConcurrency.` + ); + + is( + result.appCodeName, + CONST_APPCODENAME, + "worker Navigator.appCodeName reports correct constant value." + ); + is( + result.appName, + CONST_APPNAME, + "worker Navigator.appName reports correct constant value." + ); + is( + result.product, + CONST_PRODUCT, + "worker Navigator.product reports correct constant value." + ); + + BrowserTestUtils.removeTab(tab); + + // Ensure the content process is shut down entirely before we start the next + // test in Fission. + if (SpecialPowers.useRemoteSubframes) { + await new Promise(resolve => { + let observer = (subject, topic, data) => { + if (topic === "ipc:content-shutdown") { + Services.obs.removeObserver(observer, "ipc:content-shutdown"); + resolve(); + } + }; + Services.obs.addObserver(observer, "ipc:content-shutdown"); + }); + } +} + +add_task(async function setupDefaultUserAgent() { + let defaultUserAgent = `Mozilla/5.0 (${ + DEFAULT_UA_OS[AppConstants.platform] + }; rv:${rvVersion}.0) Gecko/${ + DEFAULT_UA_GECKO_TRAIL[AppConstants.platform] + } Firefox/${appVersion}.0`; + expectedResults = { + testDesc: "default", + appVersion: DEFAULT_APPVERSION[AppConstants.platform], + hardwareConcurrency: navigator.hardwareConcurrency, + mimeTypesLength: 2, + oscpu: DEFAULT_OSCPU[AppConstants.platform], + platform: DEFAULT_PLATFORM[AppConstants.platform], + pluginsLength: 5, + userAgentNavigator: defaultUserAgent, + userAgentHeader: defaultUserAgent, + }; + + await testNavigator(); + + await testUserAgentHeader(); + + await testWorkerNavigator(); +}); + +add_task(async function setupRFPExemptions() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + "example.net, mochi.test", + ], + ], + }); + + let defaultUserAgent = `Mozilla/5.0 (${ + DEFAULT_UA_OS[AppConstants.platform] + }; rv:${rvVersion}.0) Gecko/${ + DEFAULT_UA_GECKO_TRAIL[AppConstants.platform] + } Firefox/${appVersion}.0`; + + expectedResults = { + testDesc: "RFP Exempted Domain", + appVersion: DEFAULT_APPVERSION[AppConstants.platform], + hardwareConcurrency: navigator.hardwareConcurrency, + mimeTypesLength: 2, + oscpu: DEFAULT_OSCPU[AppConstants.platform], + platform: DEFAULT_PLATFORM[AppConstants.platform], + pluginsLength: 5, + userAgentNavigator: defaultUserAgent, + userAgentHeader: defaultUserAgent, + }; + + await testNavigator(); + + await testUserAgentHeader(); + + await testWorkerNavigator(); + + // Pop exempted domains + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function setupResistFingerprinting() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + let spoofedGeckoTrail = SPOOFED_UA_GECKO_TRAIL[AppConstants.platform]; + + let spoofedUserAgentNavigator = `Mozilla/5.0 (${ + SPOOFED_UA_NAVIGATOR_OS[AppConstants.platform] + }; rv:${rvVersion}.0) Gecko/${spoofedGeckoTrail} Firefox/${appVersion}.0`; + + let spoofedUserAgentHeader = `Mozilla/5.0 (${ + SPOOFED_UA_HTTPHEADER_OS[AppConstants.platform] + }; rv:${rvVersion}.0) Gecko/${spoofedGeckoTrail} Firefox/${appVersion}.0`; + + expectedResults = { + testDesc: "spoofed", + appVersion: SPOOFED_APPVERSION[AppConstants.platform], + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, + mimeTypesLength: 2, + oscpu: SPOOFED_OSCPU[AppConstants.platform], + platform: SPOOFED_PLATFORM[AppConstants.platform], + pluginsLength: 5, + userAgentNavigator: spoofedUserAgentNavigator, + userAgentHeader: spoofedUserAgentHeader, + }; + + await testNavigator(); + + await testUserAgentHeader(); + + await testWorkerNavigator(); +}); + +// This tests that 'general.*.override' should not override spoofed values. +add_task(async function runOverrideTest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["general.appname.override", "appName overridden"], + ["general.appversion.override", "appVersion overridden"], + ["general.platform.override", "platform overridden"], + ["general.useragent.override", "userAgent overridden"], + ["general.oscpu.override", "oscpu overridden"], + ], + }); + + await testNavigator(); + + await testWorkerNavigator(); + + await testUserAgentHeader(); + + // Pop general.appname.override etc + await SpecialPowers.popPrefEnv(); + + // Pop privacy.resistFingerprinting + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_navigator_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_navigator_iframes.js new file mode 100644 index 0000000000..6d6e836cd4 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_navigator_iframes.js @@ -0,0 +1,392 @@ +/** + * Bug 1737829 and Bug 1770498 - A test case for making sure the navigator object has been + * spoofed/disabled correctly respecting cross-origin resources, iframes + * and exemption behavior. + * + * This test only tests values in the iframe, it does not test them on the framer + * We use the cross-origin domain as the base URI of a resource we fetch (on both the framer and framee) + * so we can check that the HTTP header is as expected. + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + WindowsVersionInfo: + "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs", +}); + +let osVersion = Services.sysinfo.get("version"); +if (AppConstants.platform == "macosx") { + // Convert Darwin version to macOS version: 19.x.x -> 10.15 etc. + // https://en.wikipedia.org/wiki/Darwin_%28operating_system%29 + let DarwinVersionParts = osVersion.split("."); + let DarwinMajorVersion = +DarwinVersionParts[0]; + let macOsMinorVersion = DarwinMajorVersion - 4; + if (macOsMinorVersion > 15) { + macOsMinorVersion = 15; + } + osVersion = `10.${macOsMinorVersion}`; +} + +const DEFAULT_APPVERSION = { + linux: "5.0 (X11)", + win: "5.0 (Windows)", + macosx: "5.0 (Macintosh)", + android: `5.0 (Android ${osVersion})`, + other: "5.0 (X11)", +}; + +const SPOOFED_APPVERSION = { + linux: "5.0 (X11)", + win: "5.0 (Windows)", + macosx: "5.0 (Macintosh)", + android: "5.0 (Android 10)", + other: "5.0 (X11)", +}; + +let cpuArch = Services.sysinfo.get("arch"); +if (cpuArch == "x86-64") { + // Convert CPU arch "x86-64" to "x86_64" used in Linux and Android UAs. + cpuArch = "x86_64"; +} + +const DEFAULT_PLATFORM = { + linux: `Linux ${cpuArch}`, + win: "Win32", + macosx: "MacIntel", + android: `Linux ${cpuArch}`, + other: `Linux ${cpuArch}`, +}; + +const SPOOFED_PLATFORM = { + linux: "Linux x86_64", + win: "Win32", + macosx: "MacIntel", + android: "Linux aarch64", + other: "Linux x86_64", +}; + +// If comparison with the WindowsOscpu value fails in the future, it's time to +// evaluate if exposing a new Windows version to the Web is appropriate. See +// https://bugzilla.mozilla.org/show_bug.cgi?id=1693295 +let WindowsOscpu = null; +if (AppConstants.platform == "win") { + let isWin11 = WindowsVersionInfo.get().buildNumber >= 22000; + WindowsOscpu = + cpuArch == "x86_64" || (cpuArch == "aarch64" && isWin11) + ? `Windows NT ${osVersion}; Win64; x64` + : `Windows NT ${osVersion}`; +} + +const DEFAULT_OSCPU = { + linux: `Linux ${cpuArch}`, + win: WindowsOscpu, + macosx: `Intel Mac OS X ${osVersion}`, + android: `Linux ${cpuArch}`, + other: `Linux ${cpuArch}`, +}; + +const SPOOFED_OSCPU = { + linux: "Linux x86_64", + win: "Windows NT 10.0; Win64; x64", + macosx: "Intel Mac OS X 10.15", + android: "Linux aarch64", + other: "Linux x86_64", +}; + +const DEFAULT_UA_OS = { + linux: `X11; Linux ${cpuArch}`, + win: WindowsOscpu, + macosx: `Macintosh; Intel Mac OS X ${osVersion}`, + android: `Android ${osVersion}; Mobile`, + other: `X11; Linux ${cpuArch}`, +}; + +const SPOOFED_UA_NAVIGATOR_OS = { + linux: "X11; Linux x86_64", + win: "Windows NT 10.0; Win64; x64", + macosx: "Macintosh; Intel Mac OS X 10.15", + android: "Android 10; Mobile", + other: "X11; Linux x86_64", +}; +const SPOOFED_UA_HTTPHEADER_OS = { + linux: "Windows NT 10.0", + win: "Windows NT 10.0", + macosx: "Windows NT 10.0", + android: "Android 10; Mobile", + other: "Windows NT 10.0", +}; +const SPOOFED_HW_CONCURRENCY = 2; + +const CONST_APPCODENAME = "Mozilla"; +const CONST_APPNAME = "Netscape"; +const CONST_PRODUCT = "Gecko"; +const CONST_PRODUCTSUB = "20100101"; +const CONST_VENDOR = ""; +const CONST_VENDORSUB = ""; + +const appVersion = parseInt(Services.appinfo.version); +const rvVersion = + parseInt( + Services.prefs.getIntPref("network.http.useragent.forceRVOnly", 0), + 0 + ) || appVersion; +const spoofedVersion = AppConstants.platform == "android" ? "115" : appVersion; + +const LEGACY_UA_GECKO_TRAIL = "20100101"; + +const DEFAULT_UA_GECKO_TRAIL = { + linux: LEGACY_UA_GECKO_TRAIL, + win: LEGACY_UA_GECKO_TRAIL, + macosx: LEGACY_UA_GECKO_TRAIL, + android: `${appVersion}.0`, + other: LEGACY_UA_GECKO_TRAIL, +}; + +const SPOOFED_UA_GECKO_TRAIL = { + linux: LEGACY_UA_GECKO_TRAIL, + win: LEGACY_UA_GECKO_TRAIL, + macosx: LEGACY_UA_GECKO_TRAIL, + android: `${spoofedVersion}.0`, + other: LEGACY_UA_GECKO_TRAIL, +}; + +const DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + +// ============================================================================================= +// ============================================================================================= + +async function testNavigator(result, expectedResults, extraData) { + let testDesc = extraData.testDesc; + + is( + result.appVersion, + expectedResults.appVersion, + `Checking ${testDesc} navigator.appVersion.` + ); + is( + result.platform, + expectedResults.platform, + `Checking ${testDesc} navigator.platform.` + ); + is( + result.userAgent, + expectedResults.userAgentNavigator, + `Checking ${testDesc} navigator.userAgent.` + ); + is( + result.userAgentHTTPHeader, + expectedResults.userAgentHTTPHeader, + `Checking ${testDesc} userAgentHTTPHeader.` + ); + is( + result.framer_crossOrigin_userAgentHTTPHeader, + expectedResults.framer_crossOrigin_userAgentHTTPHeader, + `Checking ${testDesc} framer contacting cross-origin userAgentHTTPHeader.` + ); + is( + result.framee_crossOrigin_userAgentHTTPHeader, + expectedResults.framee_crossOrigin_userAgentHTTPHeader, + `Checking ${testDesc} framee contacting cross-origin userAgentHTTPHeader.` + ); + is( + result.mimeTypesLength, + expectedResults.mimeTypesLength, + `Navigator.mimeTypes has a length of ${expectedResults.mimeTypesLength}.` + ); + is( + result.pluginsLength, + expectedResults.pluginsLength, + `Navigator.plugins has a length of ${expectedResults.pluginsLength}.` + ); + is( + result.oscpu, + expectedResults.oscpu, + `Checking ${testDesc} navigator.oscpu.` + ); + is( + result.hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} navigator.hardwareConcurrency.` + ); + + is( + result.appCodeName, + CONST_APPCODENAME, + "Navigator.appCodeName reports correct constant value." + ); + is( + result.appName, + CONST_APPNAME, + "Navigator.appName reports correct constant value." + ); + is( + result.product, + CONST_PRODUCT, + "Navigator.product reports correct constant value." + ); + is( + result.productSub, + CONST_PRODUCTSUB, + "Navigator.productSub reports correct constant value." + ); + is( + result.vendor, + CONST_VENDOR, + "Navigator.vendor reports correct constant value." + ); + is( + result.vendorSub, + CONST_VENDORSUB, + "Navigator.vendorSub reports correct constant value." + ); + + is( + result.worker_appVersion, + expectedResults.appVersion, + `Checking ${testDesc} worker navigator.appVersion.` + ); + is( + result.worker_platform, + expectedResults.platform, + `Checking ${testDesc} worker navigator.platform.` + ); + is( + result.worker_userAgent, + expectedResults.userAgentNavigator, + `Checking ${testDesc} worker navigator.userAgent.` + ); + is( + result.worker_hardwareConcurrency, + expectedResults.hardwareConcurrency, + `Checking ${testDesc} worker navigator.hardwareConcurrency.` + ); + + is( + result.worker_appCodeName, + CONST_APPCODENAME, + "worker Navigator.appCodeName reports correct constant value." + ); + is( + result.worker_appName, + CONST_APPNAME, + "worker Navigator.appName reports correct constant value." + ); + is( + result.worker_product, + CONST_PRODUCT, + "worker Navigator.product reports correct constant value." + ); +} + +const defaultUserAgent = `Mozilla/5.0 (${ + DEFAULT_UA_OS[AppConstants.platform] +}; rv:${rvVersion}.0) Gecko/${ + DEFAULT_UA_GECKO_TRAIL[AppConstants.platform] +} Firefox/${appVersion}.0`; + +const spoofedUserAgentNavigator = `Mozilla/5.0 (${ + SPOOFED_UA_NAVIGATOR_OS[AppConstants.platform] +}; rv:${rvVersion}.0) Gecko/${ + SPOOFED_UA_GECKO_TRAIL[AppConstants.platform] +} Firefox/${appVersion}.0`; + +const spoofedUserAgentHeader = `Mozilla/5.0 (${ + SPOOFED_UA_HTTPHEADER_OS[AppConstants.platform] +}; rv:${rvVersion}.0) Gecko/${ + SPOOFED_UA_GECKO_TRAIL[AppConstants.platform] +} Firefox/${appVersion}.0`; + +// The following are convenience objects that allow you to quickly see what is +// and is not modified from a logical set of values. +// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a +// deep copy and avoiding corrupting the original 'const' object +const allNotSpoofed = { + appVersion: DEFAULT_APPVERSION[AppConstants.platform], + hardwareConcurrency: navigator.hardwareConcurrency, + mimeTypesLength: 2, + oscpu: DEFAULT_OSCPU[AppConstants.platform], + platform: DEFAULT_PLATFORM[AppConstants.platform], + pluginsLength: 5, + userAgentNavigator: defaultUserAgent, + userAgentHTTPHeader: defaultUserAgent, + framer_crossOrigin_userAgentHTTPHeader: defaultUserAgent, + framee_crossOrigin_userAgentHTTPHeader: defaultUserAgent, +}; +const allSpoofed = { + appVersion: SPOOFED_APPVERSION[AppConstants.platform], + hardwareConcurrency: SPOOFED_HW_CONCURRENCY, + mimeTypesLength: 2, + oscpu: SPOOFED_OSCPU[AppConstants.platform], + platform: SPOOFED_PLATFORM[AppConstants.platform], + pluginsLength: 5, + userAgentNavigator: spoofedUserAgentNavigator, + userAgentHTTPHeader: spoofedUserAgentHeader, + framer_crossOrigin_userAgentHTTPHeader: spoofedUserAgentHeader, + framee_crossOrigin_userAgentHTTPHeader: spoofedUserAgentHeader, +}; + +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_navigator_iframer.html`; + +requestLongerTimeout(2); + +let expectedResults = {}; + +expectedResults = structuredClone(allNotSpoofed); +add_task(defaultsTest.bind(null, uri, testNavigator, expectedResults)); + +expectedResults = structuredClone(allSpoofed); +add_task(simpleRFPTest.bind(null, uri, testNavigator, expectedResults)); + +// In the below tests, we use the cross-origin domain as the base URI of a resource we fetch (on both the framer and framee) +// so we can check that the HTTP header is as expected. + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testA.bind(null, uri, testNavigator, expectedResults)); + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +expectedResults = structuredClone(allNotSpoofed); +add_task(testB.bind(null, uri, testNavigator, expectedResults)); + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +expectedResults = structuredClone(allSpoofed); +expectedResults.framer_crossOrigin_userAgentHTTPHeader = defaultUserAgent; +expectedResults.framee_crossOrigin_userAgentHTTPHeader = spoofedUserAgentHeader; +add_task(testC.bind(null, uri, testNavigator, expectedResults)); + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +expectedResults.framer_crossOrigin_userAgentHTTPHeader = defaultUserAgent; +expectedResults.framee_crossOrigin_userAgentHTTPHeader = spoofedUserAgentHeader; +add_task(testD.bind(null, uri, testNavigator, expectedResults)); + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testE.bind(null, uri, testNavigator, expectedResults)); + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testF.bind(null, uri, testNavigator, expectedResults)); + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +expectedResults = structuredClone(allSpoofed); +add_task(testG.bind(null, uri, testNavigator, expectedResults)); + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +expectedResults = structuredClone(allSpoofed); +add_task(testH.bind(null, uri, testNavigator, expectedResults)); diff --git a/browser/components/resistfingerprinting/test/browser/browser_netInfo.js b/browser/components/resistfingerprinting/test/browser/browser_netInfo.js new file mode 100644 index 0000000000..c91caa5bc3 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_netInfo.js @@ -0,0 +1,64 @@ +/** + * Bug 1372072 - A test case for check whether network information API has been + * spoofed correctly when 'privacy.resistFingerprinting' is true; + */ + +async function testWindow() { + // Open a tab to test network information in a content. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_dummy.html" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + ok("connection" in content.navigator, "navigator.connection should exist"); + + is( + content.navigator.connection.type, + "unknown", + "The connection type is spoofed correctly" + ); + }); + + BrowserTestUtils.removeTab(tab); +} + +async function testWorker() { + // Open a tab to test network information in a worker. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_dummy.html" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await new Promise(resolve => { + let worker = new content.Worker("file_workerNetInfo.js"); + + worker.onmessage = function (e) { + if (e.data.type == "status") { + ok(e.data.status, e.data.msg); + } else if (e.data.type == "finish") { + resolve(); + } else { + ok(false, "Unknown message type"); + resolve(); + } + }; + worker.postMessage({ type: "runTests" }); + }); + }); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function runTest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["dom.netinfo.enabled", true], + ], + }); + + await testWindow(); + await testWorker(); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js b/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js new file mode 100644 index 0000000000..9126fe4690 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_performanceAPI.js @@ -0,0 +1,166 @@ +// ================================================================================================ +// ================================================================================================ +add_task(async function runRFPandRTPTests() { + // RFP = ResistFingerprinting / RTP = ReduceTimePrecision + let runTests = async function (data) { + let timerlist = data.list; + let labelType = data.resistFingerprinting + ? "ResistFingerprinting" + : "ReduceTimePrecision"; + let expectedPrecision = data.precision; + // eslint beleives that isrounded is available in this scope, but if you + // remove the assignment, you will see it is not + // eslint-disable-next-line + let isRounded = eval(data.isRoundedFunc); + + ok( + isRounded(content.performance.timeOrigin, expectedPrecision), + `For ${labelType}, performance.timeOrigin is not correctly rounded: ` + + content.performance.timeOrigin + ); + + // Check that whether the performance timing API is correctly spoofed. + for (let time of timerlist) { + if ( + data.resistFingerprinting && + (time == "domainLookupStart" || time == "domainLookupEnd") + ) { + is( + content.performance.timing[time], + content.performance.timing.fetchStart, + `For resistFingerprinting, the timing(${time}) is not correctly spoofed.` + ); + } else { + ok( + isRounded(content.performance.timing[time], expectedPrecision), + `For ${labelType}(` + + expectedPrecision + + `), the timing(${time}) is not correctly rounded: ` + + content.performance.timing[time] + ); + } + } + + // Try to add some entries. + content.performance.mark("Test"); + content.performance.mark("Test-End"); + content.performance.measure("Test-Measure", "Test", "Test-End"); + + // Check the entries for performance.getEntries/getEntriesByType/getEntriesByName. + await new Promise(resolve => { + const paintObserver = new content.PerformanceObserver(() => { + resolve(); + }); + paintObserver.observe({ type: "paint", buffered: true }); + }); + + is( + content.performance.getEntries().length, + 5, + `For ${labelType}, there should be 4 entries for performance.getEntries()` + // PerformancePaintTiming, PerformanceNavigationTiming, PerformanceMark, PerformanceMark, PerformanceMeasure + ); + for (var i = 0; i < 5; i++) { + let startTime = content.performance.getEntries()[i].startTime; + let duration = content.performance.getEntries()[i].duration; + ok( + isRounded(startTime, expectedPrecision), + `For ${labelType}(` + + expectedPrecision + + "), performance.getEntries(" + + i + + ").startTime is not rounded: " + + startTime + ); + ok( + isRounded(duration, expectedPrecision), + `For ${labelType}(` + + expectedPrecision + + "), performance.getEntries(" + + i + + ").duration is not rounded: " + + duration + ); + } + is( + content.performance.getEntriesByType("mark").length, + 2, + `For ${labelType}, there should be 2 entries for performance.getEntriesByType()` + ); + is( + content.performance.getEntriesByName("Test", "mark").length, + 1, + `For ${labelType}, there should be 1 entry for performance.getEntriesByName()` + ); + content.performance.clearMarks(); + content.performance.clearMeasures(); + content.performance.clearResourceTimings(); + }; + + await setupPerformanceAPISpoofAndDisableTest( + true, + true, + false, + 200, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + true, + true, + false, + 100, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + true, + false, + false, + 13, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + true, + false, + false, + 0.13, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest(true, true, true, 100, runTests); + await setupPerformanceAPISpoofAndDisableTest(true, false, true, 13, runTests); + await setupPerformanceAPISpoofAndDisableTest( + true, + false, + true, + 0.13, + runTests + ); + + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 100, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 13, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 0.13, + runTests + ); + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + true, + 0.005, + runTests + ); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_performanceAPIWorkers.js b/browser/components/resistfingerprinting/test/browser/browser_performanceAPIWorkers.js new file mode 100644 index 0000000000..0abb38d532 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_performanceAPIWorkers.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// ================================================================================================ +// ================================================================================================ +let runWorkerTest = async function (data) { + let expectedPrecision = data.precision; + let workerCall = data.workerCall; + await new Promise(resolve => { + let worker = new content.Worker("file_workerPerformance.js"); + worker.onmessage = function (e) { + if (e.data.type == "status") { + ok(e.data.status, e.data.msg); + } else if (e.data.type == "finish") { + worker.terminate(); + resolve(); + } else { + ok(false, "Unknown message type"); + worker.terminate(); + resolve(); + } + }; + worker.postMessage({ type: workerCall, precision: expectedPrecision }); + }); +}; + +add_task(async function runRFPestsForWorker() { + await setupPerformanceAPISpoofAndDisableTest( + true, + true, + false, + 100, + runWorkerTest, + "runTimerTests" + ); + await setupPerformanceAPISpoofAndDisableTest( + true, + false, + false, + 13, + runWorkerTest, + "runTimerTests" + ); + await setupPerformanceAPISpoofAndDisableTest( + true, + true, + false, + 0.13, + runWorkerTest, + "runTimerTests" + ); +}); + +add_task(async function runRTPTestsForWorker() { + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 100, + runWorkerTest, + "runTimerTests" + ); + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 13, + runWorkerTest, + "runTimerTests" + ); + await setupPerformanceAPISpoofAndDisableTest( + false, + true, + false, + 0.13, + runWorkerTest, + "runTimerTests" + ); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_reduceTimePrecision_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_reduceTimePrecision_iframes.js new file mode 100644 index 0000000000..69d32872ab --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_reduceTimePrecision_iframes.js @@ -0,0 +1,242 @@ +/** + * This test only tests values in the iframe, it does not test them on the framer + * + * Covers the following cases: + * - RFP is disabled entirely + * - RFP is enabled entirely + * + * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + */ + +"use strict"; + +requestLongerTimeout(3); + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", +}); + +// ============================================================================================= +// ============================================================================================= + +async function testTimePrecision(results, expectedResults, extraData) { + let testDesc = extraData.testDesc; + let precision = undefined; + + if (!expectedResults.shouldRFPApply) { + precision = extraData.RTP_Precision; + } else { + precision = extraData.RFP_Precision; + } + + for (let result of results) { + let isRounded = isTimeValueRounded(result.value, precision); + + ok( + isRounded, + "Test: " + + testDesc + + " - '" + + "'" + + result.name + + "' should be rounded to nearest " + + precision + + " ms; saw " + + result.value + ); + } +} + +const RFP_TIME_ATOM_MS = 16.667; +const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html`; + +// The first three variables are defined here; and then set for test banks below. +let extraData = {}; +let extraPrefs = {}; +let precision = 100; +let expectedResults = {}; // In this test, we don't have explicit expected values, but rather we expect them to be rounded + +// ======================================================================================================================== +// Create a function that defines all the tests +function addAllTests(extraData_, extraPrefs_) { + add_task( + defaultsTest.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + add_task( + simpleRFPTest.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain + add_task( + testA.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain + add_task( + testB.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee + add_task( + testC.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain + add_task( + testD.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain + add_task( + testE.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain + add_task( + testF.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain + add_task( + testG.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); + + // (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee + add_task( + testH.bind( + null, + uri, + testTimePrecision, + expectedResults, + extraData_, + extraPrefs_ + ) + ); +} + +// ======================================================================================================================== +// First we run through all the tests with RTP's precision set to 100 ms and 133 ms. +// Because 100ms and 133ms are >= RFP's precision of 100ms, _all_ tests results should +// be rounded. +precision = 100; +extraData = { + RFP_Precision: precision, + RTP_Precision: precision, +}; +extraPrefs = [ + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + precision * 1000, + ], +]; +addAllTests(extraData, extraPrefs); + +precision = 133; +extraData = { + RFP_Precision: precision, + RTP_Precision: precision, +}; +extraPrefs = [ + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + precision * 1000, + ], +]; +addAllTests(extraData, extraPrefs); + +// ======================================================================================================================== +// Then we run through all the tests with the precision set to its normal value. +// This will mean that in some cases we expect RFP to apply and in some we don't. + +precision = RFP_TIME_ATOM_MS; +extraData = { + RFP_Precision: precision, + RTP_Precision: 1, +}; +extraPrefs = []; +addAllTests(extraData, extraPrefs); + +// ======================================================================================================================== +// Finally we run through all the tests with the precision set to an unusual value +// This will mean that in some cases we expect RFP to apply and in some we don't. + +precision = RFP_TIME_ATOM_MS; +extraData = { + RFP_Precision: RFP_TIME_ATOM_MS, + RTP_Precision: 7, +}; +extraPrefs = [ + ["privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 7 * 1000], +]; +addAllTests(extraData, extraPrefs); diff --git a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_dialogWindow.js b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_dialogWindow.js new file mode 100644 index 0000000000..96ad858fb3 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_dialogWindow.js @@ -0,0 +1,51 @@ +/** + * Bug 1352305 - A test case for dialog windows that it should not be rounded + * even after fingerprinting resistance is enabled. + */ + +async function test_dialog_window() { + let diagWin; + + await new Promise(resolve => { + // Open a dialog window which is not rounded size. + diagWin = window.openDialog( + "about:blank", + null, + "innerWidth=250,innerHeight=350" + ); + + diagWin.addEventListener( + "load", + function () { + resolve(); + }, + { once: true } + ); + }); + + is(diagWin.innerWidth, 250, "The dialog window doesn't have a rounded size."); + is( + diagWin.innerHeight, + 350, + "The dialog window doesn't have a rounded size." + ); + + await BrowserTestUtils.closeWindow(diagWin); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); +}); + +add_task(test_dialog_window); + +add_task(async function test_dialog_window_without_resistFingerprinting() { + // Test dialog windows with 'privacy.resistFingerprinting' is false. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", false]], + }); + + await test_dialog_window(); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_newWindow.js b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_newWindow.js new file mode 100644 index 0000000000..ea22c7fa20 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_newWindow.js @@ -0,0 +1,62 @@ +/* + * Bug 1330882 - A test case for opening new windows as rounded size when + * fingerprinting resistance is enabled. + */ + +const CC = Components.Constructor; + +let gMaxAvailWidth; +let gMaxAvailHeight; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + // Calculate the maximum available size. + let maxAvailSize = await calcMaximumAvailSize(); + + gMaxAvailWidth = maxAvailSize.maxAvailWidth; + gMaxAvailHeight = maxAvailSize.maxAvailHeight; +}); + +add_task(async function test_new_window() { + // Open a new window. + let win = await BrowserTestUtils.openNewBrowserWindow(); + + // Load a page and verify its window size. + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_PATH + "file_dummy.html" + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ gMaxAvailWidth, gMaxAvailHeight }], + async function (input) { + is( + content.screen.width, + input.gMaxAvailWidth, + "The screen.width has a correct rounded value" + ); + is( + content.screen.height, + input.gMaxAvailHeight, + "The screen.height has a correct rounded value" + ); + is( + content.innerWidth, + input.gMaxAvailWidth, + "The window.innerWidth has a correct rounded value" + ); + is( + content.innerHeight, + input.gMaxAvailHeight, + "The window.innerHeight has a correct rounded value" + ); + } + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_max_inner.js b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_max_inner.js new file mode 100644 index 0000000000..5d8ed74580 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_max_inner.js @@ -0,0 +1,26 @@ +/* + * Bug 1330882 - A test case for opening new windows through window.open() as + * rounded size when fingerprinting resistance is enabled. This test is for + * maximum values. + */ + +OpenTest.run([ + { + settingWidth: 1025, + settingHeight: 1050, + targetWidth: 1000, + targetHeight: 1000, + }, + { + settingWidth: 9999, + settingHeight: 9999, + targetWidth: 1000, + targetHeight: 1000, + }, + { + settingWidth: 999, + settingHeight: 999, + targetWidth: 1000, + targetHeight: 1000, + }, +]); diff --git a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_mid_inner.js b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_mid_inner.js new file mode 100644 index 0000000000..a3a41829e0 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_mid_inner.js @@ -0,0 +1,26 @@ +/* + * Bug 1330882 - A test case for opening new windows through window.open() as + * rounded size when fingerprinting resistance is enabled. This test is for + * middle values. + */ + +OpenTest.run([ + { + settingWidth: 600, + settingHeight: 600, + targetWidth: 600, + targetHeight: 600, + }, + { + settingWidth: 599, + settingHeight: 599, + targetWidth: 600, + targetHeight: 600, + }, + { + settingWidth: 401, + settingHeight: 501, + targetWidth: 600, + targetHeight: 600, + }, +]); diff --git a/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_min_inner.js b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_min_inner.js new file mode 100644 index 0000000000..f016a560ba --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_roundedWindow_open_min_inner.js @@ -0,0 +1,20 @@ +/* + * Bug 1330882 - A test case for opening new windows through window.open() as + * rounded size when fingerprinting resistance is enabled. This test is for + * minimum values. + */ + +OpenTest.run([ + { + settingWidth: 199, + settingHeight: 99, + targetWidth: 200, + targetHeight: 100, + }, + { + settingWidth: 10, + settingHeight: 10, + targetWidth: 200, + targetHeight: 100, + }, +]); diff --git a/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js new file mode 100644 index 0000000000..457d5fce8b --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js @@ -0,0 +1,2268 @@ +/** + * Bug 1222285 - A test case for testing whether keyboard events be spoofed correctly + * when fingerprinting resistance is enable. + */ + +const SHOULD_DELIVER_KEYDOWN = 0x1; +const SHOULD_DELIVER_KEYPRESS = 0x2; +const SHOULD_DELIVER_KEYUP = 0x4; +const SHOULD_DELIVER_ALL_FOR_PRINTABLE = + SHOULD_DELIVER_KEYDOWN | SHOULD_DELIVER_KEYPRESS | SHOULD_DELIVER_KEYUP; +const SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE = + SHOULD_DELIVER_KEYDOWN | SHOULD_DELIVER_KEYUP; + +// The test cases for english content. +const TEST_CASES_EN = [ + { + key: "KEY_ArrowDown", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "ArrowDown", + code: "ArrowDown", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_DOWN, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_ArrowLeft", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "ArrowLeft", + code: "ArrowLeft", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_LEFT, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_ArrowRight", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "ArrowRight", + code: "ArrowRight", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_RIGHT, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_ArrowUp", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "ArrowUp", + code: "ArrowUp", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_UP, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_CapsLock", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_KEYDOWN, + result: { + key: "CapsLock", + code: "CapsLock", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_CAPS_LOCK, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_End", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "End", + code: "End", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_END, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_Enter", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "Enter", + code: "Enter", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_RETURN, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_Escape", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "Escape", + code: "Escape", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_ESCAPE, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_Home", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "Home", + code: "Home", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_HOME, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_Meta", + modifiers: { location: KeyboardEvent.DOM_KEY_LOCATION_LEFT, metaKey: true }, + expectedKeyEvent: SHOULD_DELIVER_KEYDOWN, + result: { + key: "Meta", + code: "OSLeft", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_WIN, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_Meta", + modifiers: { + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + metaKey: true, + }, + expectedKeyEvent: SHOULD_DELIVER_KEYDOWN, + result: { + key: "Meta", + code: "OSRight", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_WIN, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_OS", + modifiers: { location: KeyboardEvent.DOM_KEY_LOCATION_LEFT, osKey: true }, + expectedKeyEvent: SHOULD_DELIVER_KEYDOWN, + result: { + key: "OS", + code: "OSLeft", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_WIN, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_OS", + modifiers: { location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT, osKey: true }, + expectedKeyEvent: SHOULD_DELIVER_KEYDOWN, + result: { + key: "OS", + code: "OSRight", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_WIN, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_PageDown", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "PageDown", + code: "PageDown", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_PAGE_DOWN, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_PageUp", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "PageUp", + code: "PageUp", + charCode: 0, + keyCode: KeyboardEvent.DOM_VK_PAGE_UP, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: " ", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: " ", + code: "Space", + charCode: 32, + keyCode: KeyboardEvent.DOM_VK_SPACE, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ",", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ",", + code: "Comma", + charCode: 44, + keyCode: KeyboardEvent.DOM_VK_COMMA, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "<", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "<", + code: "Comma", + charCode: 60, + keyCode: KeyboardEvent.DOM_VK_COMMA, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "[", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "[", + code: "BracketLeft", + charCode: 91, + keyCode: KeyboardEvent.DOM_VK_OPEN_BRACKET, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "{", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "{", + code: "BracketLeft", + charCode: 123, + keyCode: KeyboardEvent.DOM_VK_OPEN_BRACKET, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "]", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "]", + code: "BracketRight", + charCode: 93, + keyCode: KeyboardEvent.DOM_VK_CLOSE_BRACKET, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "}", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "}", + code: "BracketRight", + charCode: 125, + keyCode: KeyboardEvent.DOM_VK_CLOSE_BRACKET, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "\\", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "\\", + code: "Backslash", + charCode: 92, + keyCode: KeyboardEvent.DOM_VK_BACK_SLASH, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "|", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "|", + code: "Backslash", + charCode: 124, + keyCode: KeyboardEvent.DOM_VK_BACK_SLASH, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ";", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ";", + code: "Semicolon", + charCode: 59, + keyCode: KeyboardEvent.DOM_VK_SEMICOLON, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ":", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ":", + code: "Semicolon", + charCode: 58, + keyCode: KeyboardEvent.DOM_VK_SEMICOLON, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ".", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ".", + code: "Period", + charCode: 46, + keyCode: KeyboardEvent.DOM_VK_PERIOD, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ">", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ">", + code: "Period", + charCode: 62, + keyCode: KeyboardEvent.DOM_VK_PERIOD, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "/", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "/", + code: "Slash", + charCode: 47, + keyCode: KeyboardEvent.DOM_VK_SLASH, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "?", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "?", + code: "Slash", + charCode: 63, + keyCode: KeyboardEvent.DOM_VK_SLASH, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "'", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "'", + code: "Quote", + charCode: 39, + keyCode: KeyboardEvent.DOM_VK_QUOTE, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: '"', + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: '"', + code: "Quote", + charCode: 34, + keyCode: KeyboardEvent.DOM_VK_QUOTE, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "-", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "-", + code: "Minus", + charCode: 45, + keyCode: KeyboardEvent.DOM_VK_HYPHEN_MINUS, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "_", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "_", + code: "Minus", + charCode: 95, + keyCode: KeyboardEvent.DOM_VK_HYPHEN_MINUS, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "=", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "=", + code: "Equal", + charCode: 61, + keyCode: KeyboardEvent.DOM_VK_EQUALS, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "+", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "+", + code: "Equal", + charCode: 43, + keyCode: KeyboardEvent.DOM_VK_EQUALS, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "a", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "a", + code: "KeyA", + charCode: 97, + keyCode: KeyboardEvent.DOM_VK_A, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "A", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "A", + code: "KeyA", + charCode: 65, + keyCode: KeyboardEvent.DOM_VK_A, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "b", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "b", + code: "KeyB", + charCode: 98, + keyCode: KeyboardEvent.DOM_VK_B, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "B", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "B", + code: "KeyB", + charCode: 66, + keyCode: KeyboardEvent.DOM_VK_B, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "c", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "c", + code: "KeyC", + charCode: 99, + keyCode: KeyboardEvent.DOM_VK_C, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "C", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "C", + code: "KeyC", + charCode: 67, + keyCode: KeyboardEvent.DOM_VK_C, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "d", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "d", + code: "KeyD", + charCode: 100, + keyCode: KeyboardEvent.DOM_VK_D, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "D", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "D", + code: "KeyD", + charCode: 68, + keyCode: KeyboardEvent.DOM_VK_D, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "e", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "e", + code: "KeyE", + charCode: 101, + keyCode: KeyboardEvent.DOM_VK_E, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "E", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "E", + code: "KeyE", + charCode: 69, + keyCode: KeyboardEvent.DOM_VK_E, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "f", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "f", + code: "KeyF", + charCode: 102, + keyCode: KeyboardEvent.DOM_VK_F, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "F", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "F", + code: "KeyF", + charCode: 70, + keyCode: KeyboardEvent.DOM_VK_F, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "g", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "g", + code: "KeyG", + charCode: 103, + keyCode: KeyboardEvent.DOM_VK_G, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "G", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "G", + code: "KeyG", + charCode: 71, + keyCode: KeyboardEvent.DOM_VK_G, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "h", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "h", + code: "KeyH", + charCode: 104, + keyCode: KeyboardEvent.DOM_VK_H, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "H", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "H", + code: "KeyH", + charCode: 72, + keyCode: KeyboardEvent.DOM_VK_H, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "i", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "i", + code: "KeyI", + charCode: 105, + keyCode: KeyboardEvent.DOM_VK_I, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "I", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "I", + code: "KeyI", + charCode: 73, + keyCode: KeyboardEvent.DOM_VK_I, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "j", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "j", + code: "KeyJ", + charCode: 106, + keyCode: KeyboardEvent.DOM_VK_J, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "J", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "J", + code: "KeyJ", + charCode: 74, + keyCode: KeyboardEvent.DOM_VK_J, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "k", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "k", + code: "KeyK", + charCode: 107, + keyCode: KeyboardEvent.DOM_VK_K, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "K", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "K", + code: "KeyK", + charCode: 75, + keyCode: KeyboardEvent.DOM_VK_K, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "l", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "l", + code: "KeyL", + charCode: 108, + keyCode: KeyboardEvent.DOM_VK_L, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "L", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "L", + code: "KeyL", + charCode: 76, + keyCode: KeyboardEvent.DOM_VK_L, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "m", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "m", + code: "KeyM", + charCode: 109, + keyCode: KeyboardEvent.DOM_VK_M, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "M", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "M", + code: "KeyM", + charCode: 77, + keyCode: KeyboardEvent.DOM_VK_M, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "n", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "n", + code: "KeyN", + charCode: 110, + keyCode: KeyboardEvent.DOM_VK_N, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "N", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "N", + code: "KeyN", + charCode: 78, + keyCode: KeyboardEvent.DOM_VK_N, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "o", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "o", + code: "KeyO", + charCode: 111, + keyCode: KeyboardEvent.DOM_VK_O, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "O", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "O", + code: "KeyO", + charCode: 79, + keyCode: KeyboardEvent.DOM_VK_O, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "p", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "p", + code: "KeyP", + charCode: 112, + keyCode: KeyboardEvent.DOM_VK_P, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "P", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "P", + code: "KeyP", + charCode: 80, + keyCode: KeyboardEvent.DOM_VK_P, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "q", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "q", + code: "KeyQ", + charCode: 113, + keyCode: KeyboardEvent.DOM_VK_Q, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "Q", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "Q", + code: "KeyQ", + charCode: 81, + keyCode: KeyboardEvent.DOM_VK_Q, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "r", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "r", + code: "KeyR", + charCode: 114, + keyCode: KeyboardEvent.DOM_VK_R, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "R", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "R", + code: "KeyR", + charCode: 82, + keyCode: KeyboardEvent.DOM_VK_R, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "s", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "s", + code: "KeyS", + charCode: 115, + keyCode: KeyboardEvent.DOM_VK_S, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "S", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "S", + code: "KeyS", + charCode: 83, + keyCode: KeyboardEvent.DOM_VK_S, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "t", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "t", + code: "KeyT", + charCode: 116, + keyCode: KeyboardEvent.DOM_VK_T, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "T", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "T", + code: "KeyT", + charCode: 84, + keyCode: KeyboardEvent.DOM_VK_T, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "u", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "u", + code: "KeyU", + charCode: 117, + keyCode: KeyboardEvent.DOM_VK_U, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "U", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "U", + code: "KeyU", + charCode: 85, + keyCode: KeyboardEvent.DOM_VK_U, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "v", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "v", + code: "KeyV", + charCode: 118, + keyCode: KeyboardEvent.DOM_VK_V, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "V", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "V", + code: "KeyV", + charCode: 86, + keyCode: KeyboardEvent.DOM_VK_V, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "w", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "w", + code: "KeyW", + charCode: 119, + keyCode: KeyboardEvent.DOM_VK_W, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "W", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "W", + code: "KeyW", + charCode: 87, + keyCode: KeyboardEvent.DOM_VK_W, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "x", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "x", + code: "KeyX", + charCode: 120, + keyCode: KeyboardEvent.DOM_VK_X, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "X", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "X", + code: "KeyX", + charCode: 88, + keyCode: KeyboardEvent.DOM_VK_X, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "y", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "y", + code: "KeyY", + charCode: 121, + keyCode: KeyboardEvent.DOM_VK_Y, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "Y", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "Y", + code: "KeyY", + charCode: 89, + keyCode: KeyboardEvent.DOM_VK_Y, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "z", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "z", + code: "KeyZ", + charCode: 122, + keyCode: KeyboardEvent.DOM_VK_Z, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "Z", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "Z", + code: "KeyZ", + charCode: 90, + keyCode: KeyboardEvent.DOM_VK_Z, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "0", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "0", + code: "Digit0", + charCode: 48, + keyCode: KeyboardEvent.DOM_VK_0, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "1", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "1", + code: "Digit1", + charCode: 49, + keyCode: KeyboardEvent.DOM_VK_1, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "2", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "2", + code: "Digit2", + charCode: 50, + keyCode: KeyboardEvent.DOM_VK_2, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "3", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "3", + code: "Digit3", + charCode: 51, + keyCode: KeyboardEvent.DOM_VK_3, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "4", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "4", + code: "Digit4", + charCode: 52, + keyCode: KeyboardEvent.DOM_VK_4, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "5", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "5", + code: "Digit5", + charCode: 53, + keyCode: KeyboardEvent.DOM_VK_5, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "6", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "6", + code: "Digit6", + charCode: 54, + keyCode: KeyboardEvent.DOM_VK_6, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "7", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "7", + code: "Digit7", + charCode: 55, + keyCode: KeyboardEvent.DOM_VK_7, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "8", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "8", + code: "Digit8", + charCode: 56, + keyCode: KeyboardEvent.DOM_VK_8, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "9", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "9", + code: "Digit9", + charCode: 57, + keyCode: KeyboardEvent.DOM_VK_9, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: ")", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: ")", + code: "Digit0", + charCode: 41, + keyCode: KeyboardEvent.DOM_VK_0, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "!", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "!", + code: "Digit1", + charCode: 33, + keyCode: KeyboardEvent.DOM_VK_1, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "@", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "@", + code: "Digit2", + charCode: 64, + keyCode: KeyboardEvent.DOM_VK_2, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "#", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "#", + code: "Digit3", + charCode: 35, + keyCode: KeyboardEvent.DOM_VK_3, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "$", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "$", + code: "Digit4", + charCode: 36, + keyCode: KeyboardEvent.DOM_VK_4, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "%", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "%", + code: "Digit5", + charCode: 37, + keyCode: KeyboardEvent.DOM_VK_5, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "^", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "^", + code: "Digit6", + charCode: 94, + keyCode: KeyboardEvent.DOM_VK_6, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "&", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "&", + code: "Digit7", + charCode: 38, + keyCode: KeyboardEvent.DOM_VK_7, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "*", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "*", + code: "Digit8", + charCode: 42, + keyCode: KeyboardEvent.DOM_VK_8, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "(", + modifiers: { shiftKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "(", + code: "Digit9", + charCode: 40, + keyCode: KeyboardEvent.DOM_VK_9, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: true, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F1", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F1", + code: "F1", + charCode: 112, + keyCode: KeyboardEvent.DOM_VK_F1, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F2", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F2", + code: "F2", + charCode: 113, + keyCode: KeyboardEvent.DOM_VK_F2, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F3", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F3", + code: "F3", + charCode: 114, + keyCode: KeyboardEvent.DOM_VK_F3, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F4", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F4", + code: "F4", + charCode: 115, + keyCode: KeyboardEvent.DOM_VK_F4, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F5", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F5", + code: "F5", + charCode: 116, + keyCode: KeyboardEvent.DOM_VK_F5, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F7", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F7", + code: "F7", + charCode: 118, + keyCode: KeyboardEvent.DOM_VK_F7, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F8", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F8", + code: "F8", + charCode: 119, + keyCode: KeyboardEvent.DOM_VK_F8, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F9", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F9", + code: "F9", + charCode: 120, + keyCode: KeyboardEvent.DOM_VK_F9, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F10", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F10", + code: "F10", + charCode: 121, + keyCode: KeyboardEvent.DOM_VK_F10, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F11", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F11", + code: "F11", + charCode: 122, + keyCode: KeyboardEvent.DOM_VK_F11, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "KEY_F12", + modifiers: {}, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "F12", + code: "F12", + charCode: 123, + keyCode: KeyboardEvent.DOM_VK_F12, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, + { + key: "a", + modifiers: { ctrlKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "a", + code: "KeyA", + charCode: 97, + keyCode: KeyboardEvent.DOM_VK_A, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: true, + altGraphKey: false, + }, + }, + { + key: "a", + modifiers: { altKey: true }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_NON_PRINTABLE, + result: { + key: "a", + code: "KeyA", + charCode: 97, + keyCode: KeyboardEvent.DOM_VK_A, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }, +]; + +async function testKeyEvent(aTab, aTestCase) { + // Prepare all expected key events. + let testEvents = []; + + if (aTestCase.expectedKeyEvent & SHOULD_DELIVER_KEYDOWN) { + testEvents.push("keydown"); + } + + if (aTestCase.expectedKeyEvent & SHOULD_DELIVER_KEYPRESS) { + testEvents.push("keypress"); + } + + if (aTestCase.expectedKeyEvent & SHOULD_DELIVER_KEYUP) { + testEvents.push("keyup"); + } + + let allKeyEventPromises = []; + + for (let testEvent of testEvents) { + let keyEventPromise = ContentTask.spawn( + aTab.linkedBrowser, + { testEvent, result: aTestCase.result }, + async aInput => { + function verifyKeyboardEvent( + aEvent, + aResult, + aSameKeyCodeAndCharCodeValue + ) { + is( + aEvent.key, + aResult.key, + "KeyboardEvent.key is correctly spoofed." + ); + is( + aEvent.code, + aResult.code, + "KeyboardEvent.code is correctly spoofed." + ); + is( + aEvent.location, + aResult.location, + "KeyboardEvent.location is correctly spoofed." + ); + is( + aEvent.altKey, + aResult.altKey, + "KeyboardEvent.altKey is correctly spoofed." + ); + is( + aEvent.shiftKey, + aResult.shiftKey, + "KeyboardEvent.shiftKey is correctly spoofed." + ); + is( + aEvent.ctrlKey, + aResult.ctrlKey, + "KeyboardEvent.ctrlKey is correctly spoofed." + ); + + // If the charCode is not 0, this is a character. The keyCode will be remained as 0. + // Otherwise, we should check the keyCode. + if (!aSameKeyCodeAndCharCodeValue) { + if (aEvent.charCode != 0) { + is( + aEvent.keyCode, + 0, + "KeyboardEvent.keyCode should be 0 for this case." + ); + is( + aEvent.charCode, + aResult.charCode, + "KeyboardEvent.charCode is correctly spoofed." + ); + } else { + is( + aEvent.keyCode, + aResult.keyCode, + "KeyboardEvent.keyCode is correctly spoofed." + ); + is( + aEvent.charCode, + 0, + "KeyboardEvent.charCode should be 0 for this case." + ); + } + } else if (aResult.charCode) { + is( + aEvent.keyCode, + aResult.charCode, + "KeyboardEvent.keyCode should be same as expected charCode for this case." + ); + is( + aEvent.charCode, + aResult.charCode, + "KeyboardEvent.charCode is correctly spoofed." + ); + } else { + is( + aEvent.keyCode, + aResult.keyCode, + "KeyboardEvent.keyCode is correctly spoofed." + ); + is( + aEvent.charCode, + aResult.keyCode, + "KeyboardEvent.charCode should be same as expected keyCode for this case." + ); + } + + // Check getModifierState(). + is( + aEvent.modifierState.Alt, + aResult.altKey, + "KeyboardEvent.getModifierState() reports a correctly spoofed value for 'Alt'." + ); + is( + aEvent.modifierState.AltGraph, + aResult.altGraphKey, + "KeyboardEvent.getModifierState() reports a correctly spoofed value for 'AltGraph'." + ); + is( + aEvent.modifierState.Shift, + aResult.shiftKey, + `KeyboardEvent.getModifierState() reports a correctly spoofed value for 'Shift'.` + ); + is( + aEvent.modifierState.Control, + aResult.ctrlKey, + `KeyboardEvent.getModifierState() reports a correctly spoofed value for 'Control'.` + ); + } + + let { testEvent: eventType, result } = aInput; + let inputBox = content.document.getElementById("test"); + + // We need to put the real access of event object into the content page instead of + // here, ContentTask.spawn, since the script running here is under chrome privilege. + // So the fingerprinting resistance won't work here. + let resElement = content.document.getElementById("result-" + eventType); + + // First, try to focus on the input box. + await new Promise(resolve => { + if (content.document.activeElement == inputBox) { + // the input box already got focused. + resolve(); + } else { + inputBox.onfocus = () => { + resolve(); + }; + inputBox.focus(); + } + }); + + // Once the result of the keyboard event ready, the content page will send + // a custom event 'resultAvailable' for informing the script to check the + // result. + await new Promise(resolve => { + function eventHandler(aEvent) { + verifyKeyboardEvent( + JSON.parse(resElement.value), + result, + eventType == "keypress" + ); + resElement.removeEventListener( + "resultAvailable", + eventHandler, + true + ); + resolve(); + } + + resElement.addEventListener("resultAvailable", eventHandler, true); + }); + } + ); + + allKeyEventPromises.push(keyEventPromise); + } + + // Send key event to the tab. + BrowserTestUtils.synthesizeKey( + aTestCase.key, + aTestCase.modifiers, + aTab.linkedBrowser + ); + + await Promise.all(allKeyEventPromises); +} + +function eventConsumer(aEvent) { + aEvent.preventDefault(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); +}); + +add_task(async function runTestsForEnglishContent() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_keyBoardEvent.sjs?language=en-US" + ); + + // Prevent shortcut keys. + gBrowser.addEventListener("keypress", eventConsumer, true); + + for (let test of TEST_CASES_EN) { + await testKeyEvent(tab, test); + } + + // Test a key which doesn't exist in US English keyboard layout. + await testKeyEvent(tab, { + key: "\u00DF", + modifiers: { code: "Minus", keyCode: 63 }, + expectedKeyEvent: SHOULD_DELIVER_ALL_FOR_PRINTABLE, + result: { + key: "\u00DF", + code: "", + charCode: 223, + keyCode: 0, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, + altKey: false, + shiftKey: false, + ctrlKey: false, + altGraphKey: false, + }, + }); + + gBrowser.removeEventListener("keypress", eventConsumer, true); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function runTestForSuppressModifierKeys() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_keyBoardEvent.sjs?language=en-US" + ); + + // Prevent Alt key to trigger the menu item. + gBrowser.addEventListener("keydown", eventConsumer, true); + + for (let eventType of ["keydown", "keyup"]) { + for (let modifierKey of ["Alt", "Shift", "Control"]) { + let testPromise = ContentTask.spawn( + tab.linkedBrowser, + eventType, + async aEventType => { + let inputBox = content.document.getElementById("test"); + + // First, try to focus on the input box. + await new Promise(resolve => { + if (content.document.activeElement == inputBox) { + // the input box already got focused. + resolve(); + } else { + inputBox.onfocus = () => { + resolve(); + }; + inputBox.focus(); + } + }); + + let event = await new Promise(resolve => { + inputBox.addEventListener( + aEventType, + aEvent => { + resolve(aEvent); + }, + { once: true } + ); + }); + + is( + event.key, + "x", + "'x' should be seen and the modifier key should be suppressed" + ); + } + ); + + let modifierState; + + if (modifierKey === "Alt") { + modifierState = { altKey: true }; + } else if (modifierKey === "Shift") { + modifierState = { shiftKey: true }; + } else { + modifierState = { ctrlKey: true }; + } + + // Generate a Alt or Shift key event. + BrowserTestUtils.synthesizeKey( + `KEY_${modifierKey}`, + modifierState, + tab.linkedBrowser + ); + + // Generate a dummy "x" key event that will only be handled if + // modifier key is successfully suppressed. + BrowserTestUtils.synthesizeKey("x", {}, tab.linkedBrowser); + + await testPromise; + } + } + + gBrowser.removeEventListener("keydown", eventConsumer, true); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/resistfingerprinting/test/browser/browser_timezone.js b/browser/components/resistfingerprinting/test/browser/browser_timezone.js new file mode 100644 index 0000000000..66647ac787 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/browser_timezone.js @@ -0,0 +1,154 @@ +/** + * Bug 1330890 - A test case for verifying Date() object of javascript will use + * UTC timezone after fingerprinting resistance is enabled. + */ + +async function verifySpoofed() { + ok(true, "Running on " + content.location.origin); + + SpecialPowers.Cu.getJSTestingFunctions().setTimeZone("PST8PDT"); + is( + Intl.DateTimeFormat("en-US").resolvedOptions().timeZone, + "PST8PDT", + "Default time zone should have changed" + ); + + // Running in content: + function test() { + let date = new Date(); + ok( + date.toString().endsWith("(Coordinated Universal Time)"), + "The date toString() is in UTC timezone." + ); + ok( + date.toTimeString().endsWith("(Coordinated Universal Time)"), + "The date toTimeString() is in UTC timezone." + ); + let dateTimeFormat = Intl.DateTimeFormat("en-US", { + dateStyle: "full", + timeStyle: "full", + }); + is( + dateTimeFormat.resolvedOptions().timeZone, + "UTC", + "The Intl.DateTimeFormat is in UTC timezone." + ); + ok( + dateTimeFormat.format(date).endsWith("Coordinated Universal Time"), + "The Intl.DateTimeFormat is formatting with the UTC timezone." + ); + is( + date.getFullYear(), + date.getUTCFullYear(), + "The full year reports in UTC timezone." + ); + is( + date.getMonth(), + date.getUTCMonth(), + "The month reports in UTC timezone." + ); + is(date.getDate(), date.getUTCDate(), "The month reports in UTC timezone."); + is(date.getDay(), date.getUTCDay(), "The day reports in UTC timezone."); + is( + date.getHours(), + date.getUTCHours(), + "The hours reports in UTC timezone." + ); + is(date.getTimezoneOffset(), 0, "The difference with UTC timezone is 0."); + } + + // Run test in the context of the page. + Cu.exportFunction(is, content, { defineAs: "is" }); + Cu.exportFunction(ok, content, { defineAs: "ok" }); + content.eval(`(${test})()`); +} + +add_task(async function test_timezone() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + // Load a page and verify the timezone. + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_dummy.html", + forceNewProcess: true, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], verifySpoofed); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); + +// Verify that exempted domain is not spoofed. +add_task(async function test_timezone_exempt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.exemptedDomains", "example.net"], + ["privacy.resistFingerprinting", true], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_dummy.html", + forceNewProcess: true, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + SpecialPowers.Cu.getJSTestingFunctions().setTimeZone("PST8PDT"); + is( + Intl.DateTimeFormat("en-US").resolvedOptions().timeZone, + "PST8PDT", + "Default time zone should have changed" + ); + + function test() { + let date = new Date(0); + ok( + date.toString().endsWith("(Pacific Standard Time)"), + "The date toString() is in PT timezone" + ); + + is( + Intl.DateTimeFormat("en-US").resolvedOptions().timeZone, + "PST8PDT", + "Content should use default time zone" + ); + } + + // Run test in the context of the page. + Cu.exportFunction(is, content, { defineAs: "is" }); + Cu.exportFunction(ok, content, { defineAs: "ok" }); + content.eval(`(${test})()`); + }); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); + +// Verify that we are still spoofing for domains not `exemptedDomains` list. +add_task(async function test_timezone_exempt_wrong_domain() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.exemptedDomains", "example.net"], + ["privacy.resistFingerprinting", true], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: + TEST_PATH.replace("example.net", "example.org") + "file_dummy.html", + forceNewProcess: true, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], verifySpoofed); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/resistfingerprinting/test/browser/coop_header.sjs b/browser/components/resistfingerprinting/test/browser/coop_header.sjs new file mode 100644 index 0000000000..16c8eded66 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/coop_header.sjs @@ -0,0 +1,77 @@ +const HTML_DATA = ` + <!DOCTYPE HTML> + <html> + <head> + <title>Dummy test page</title> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> + </head> + <body> + <p>Dummy test page</p> + <div id="testDiv">test</div> + </body> + </html> + `; + +const WORKER = ` + self.onmessage = function(e) { + if (e.data.type == "runCmdAndGetResult") { + let result = eval(e.data.cmd); + + postMessage({ type: "result", resultOf: e.data.cmd, result: result }); + return; + } else if (e.data.type == "runCmds") { + for (let cmd of e.data.cmds) { + eval(cmd); + } + + postMessage({ type: "result", resultOf: "entriesLength", result: performance.getEntries().length }); + return; + } else if (e.data.type == "getResult") { + if (e.data.resultOf == "startTimeAndDuration") { + postMessage({ type: "result", + resultOf: "startTimeAndDuration", + result: { + index: e.data.num, + startTime: performance.getEntries()[e.data.num].startTime, + duration: performance.getEntries()[e.data.num].duration, + } + }); + return; + } else if (e.data.resultOf == "getEntriesByTypeLength") { + postMessage({ + type: "result", + resultOf: "entriesByTypeLength", + result: { + markLength: performance.getEntriesByType("mark").length, + resourceLength: performance.getEntriesByType("resource").length, + testAndMarkLength: performance.getEntriesByName("Test", "mark").length, + } + }); + return; + } + } + + postMessage({ type: "unexpectedMessageType", data: e.data.type }); + }; + `; + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + if (query.get("crossOriginIsolated") === "true") { + response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false); + } + + if (query.get("worker")) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/javascript"); + response.write(WORKER); + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + response.write(HTML_DATA); +} diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html new file mode 100644 index 0000000000..da86656bd4 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<head> +<meta charset="utf8"> +<script> +function waitForCondition(aCond, aCallback, aErrorMsg) { + var tries = 0; + var interval = setInterval(() => { + if (tries >= 30) { + result.push({ + 'error': `Exceeded 30 tries waiting for animation` + }) + clearInterval(interval); + parent.postMessage(result, "*"); + return; + } + + var conditionPassed; + try { + conditionPassed = aCond(); + } catch (e) { + result.push({ + 'error': `${e}\n${e.stack}` + }) + clearInterval(interval); + parent.postMessage(result, "*") + return; + } + + if (conditionPassed) { + clearInterval(interval); + aCallback(); + } + + tries++; + }, 100); + } + +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +var result = []; + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + + const testDiv = document.getElementById("testDiv"); + const animation = testDiv.animate({ opacity: [0, 1] }, 100000); + animation.play(); + + waitForCondition( + () => animation.currentTime > 100, + () => { + + result.push({ + 'name': 'animation.startTime', + 'value': animation.startTime + }); + result.push({ + 'name': 'animation.currentTime', + 'value': animation.currentTime + }); + result.push({ + 'name': 'animation.timeline.currentTime', + 'value': animation.timeline.currentTime + }); + + if (document.timeline) { + result.push({ + 'name': 'document.timeline.currentTime', + 'value': document.timeline.currentTime + }); + } + + parent.postMessage(result, "*") + }, + "animation failed to start"); + } +}); +</script> +</head> +<body> +<div id="testDiv">test</div> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html new file mode 100644 index 0000000000..234661a6a9 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain, extraData) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_dummy.html b/browser/components/resistfingerprinting/test/browser/file_dummy.html new file mode 100644 index 0000000000..000a9a9694 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_dummy.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframee.html new file mode 100644 index 0000000000..79edbefe24 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframee.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + var iframe = document.createElement("iframe"); + iframe.src = "about:blank?foo"; + document.body.append(iframe); + + function test() { + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + + window.parent.document.querySelector("#result").textContent = JSON.stringify(result); + } + + iframe.contentWindow.eval(`(${test})()`); + + parent.postMessage(JSON.parse(document.querySelector("#result").textContent), "*") + } +}); +</script> +<output id="result"></output> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframer.html new file mode 100644 index 0000000000..89725fe157 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html new file mode 100644 index 0000000000..23fd058c44 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script src="shared_test_funcs.js"></script> +<script> +var popup = undefined; +function createPopup() { + if(popup === undefined) { + console.log("TKTK: Creating popup"); + let s = ` + window.addEventListener('message', async function listener(event) { + if (event.data[0] == 'popup_is_ready') { + window.opener.postMessage(["popup_ready"], "*"); + } else if (event.data[0] == 'popup_request') { + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + window.opener.postMessage(['popup_response', result], '*'); + window.close(); + } + }); + setInterval(function() { + if(!window.opener || window.opener.closed) { + window.close(); + } + }, 50);`; + + popup = window.open("about:blank", ""); + popup.eval(s); + } else { + console.log("TKTK: popup already exists"); + } +} +window.addEventListener("load", createPopup); +console.log("TKTK: Adding initial load"); + +async function runTheTest(iframe_domain, cross_origin_domain, mode) { + await new Promise(r => setTimeout(r, 2000)); + console.log("TKTK: runTheTest() popup =", (popup === undefined ? "undefined" : "something")); + if (document.readyState !== 'complete') { + console.log("TKTK: !== complete"); + createPopup(); + } else if(popup === undefined) { + console.log("TKTK: popup is undefined"); + createPopup(); + } + console.log("TKTK: now popup =", (popup === undefined ? "undefined" : "something")); + popup.postMessage(["popup_is_ready", cross_origin_domain], "*"); + await waitForMessage("popup_ready", `*`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + resolve(event.data[1]); + }, { once: true }); + }); + + popup.postMessage(["popup_request", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + popup.close(); + + return result; +} + +</script> +<output id="result"></output> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframee.html new file mode 100644 index 0000000000..79958d0a3f --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframee.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + var s = `<html><script> + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + window.parent.document.querySelector('#result').textContent = JSON.stringify(result); + window.parent.postMessage(["frame_response"], "*");`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + var iframe = document.createElement("iframe"); + iframe.srcdoc = s; + document.body.append(iframe); + + } else if (event.data[0] == "frame_response") { + let result = document.querySelector("#result").textContent; + parent.postMessage(JSON.parse(result), "*"); + } +}); +</script> +<output id="result"></output> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframer.html new file mode 100644 index 0000000000..2f44be0f5a --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutsrcdoc_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframee.html new file mode 100644 index 0000000000..adc2e93789 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframee.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script type="text/javascript"> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { +//window.addEventListener("load", async function listener(event) { + if (event.data[0] == "gimme") { + // eslint-disable-next-line + var s = `<html><script> + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + window.parent.document.querySelector('#result').textContent = JSON.stringify(result); + window.parent.postMessage(["frame_response"], "*");`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + let b = new Blob([s], { type: "text/html" }); + let url = URL.createObjectURL(b); + + var iframe = document.createElement("iframe"); + iframe.src = url; + document.body.append(iframe); + } else if (event.data[0] == "frame_response") { + let result = document.querySelector("#result").textContent; + parent.postMessage(JSON.parse(result), "*"); + } +}); +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframer.html new file mode 100644 index 0000000000..e8e1c22990 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html new file mode 100644 index 0000000000..ae08111e61 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script src="shared_test_funcs.js"></script> +<script type="text/javascript"> +var popup; +async function runTheTest(iframe_domain, cross_origin_domain, mode) { + let s = `<html><script> + console.log("TKTK: Loaded popup"); + function give_result() { + console.log("TKTK: popup: give_result()"); + return { + hardwareConcurrency : navigator.hardwareConcurrency + }; + } + window.addEventListener('load', async function listener(event) { + console.log("TKTK: popup: loaded"); + window.opener.postMessage(["popup_ready"], "*"); + }); + window.addEventListener('message', async function listener(event) { + console.log("TKTK: popup: message"); + if (event.data[0] == 'popup_request') { + console.log("TKTK: popup: popup_request"); + let result = give_result(); + window.opener.postMessage(['popup_response', result], '*'); + window.close(); + } + });`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + let params = new URLSearchParams(document.location.search); + let options = ""; + if (params.get("submode") == "noopener") { + options = "noopener"; + } + + let b = new Blob([s], { type: "text/html" }); + let url = URL.createObjectURL(b); + popup = window.open(url, "", options); + + if (params.get("submode") == "noopener") { + return {}; + } + + console.log("TKTK: popup created"); + await waitForMessage("popup_ready", `*`); + console.log("TKTK: got ready message"); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + console.log("TKTK: got response message"); + resolve(event.data[1]); + }, { once: true }); + }); + + popup.postMessage(["popup_request", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + popup.close(); + console.log("TKTK: closed popup"); + + return result; +} +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html new file mode 100644 index 0000000000..235a7651a8 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script type="text/javascript"> + + var s = `<html><script> + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + window.parent.postMessage(result, "*");`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + let b = new Blob([s], { type: "text/html" }); + let url = URL.createObjectURL(b); + location.href = url; + +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html new file mode 100644 index 0000000000..d03c514fc7 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + // Set up the frame + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html`; + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + var result = await promiseForRFPTest; + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframee.html new file mode 100644 index 0000000000..befd43091a --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframee.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script type="text/javascript"> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + var s = `<html><script> + window.addEventListener("load", async function listener(event) { + parent.postMessage(["frame_ready"], "*"); + }); + window.addEventListener('message', async function listener(event) { + if (event.data[0] == 'frame_request') { + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + parent.postMessage(['frame_response', result], '*'); + } + });`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + let iframe = document.createElement("iframe"); + iframe.src = "data:text/html;charset=utf-8," + s; + document.body.append(iframe); + } else if (event.data[0] == "frame_ready") { + let iframe = document.getElementsByTagName("iframe")[0]; + iframe.contentWindow.postMessage(["frame_request"], "*"); + } else if (event.data[0] == "frame_response") { + parent.postMessage(event.data[1], "*") + } +}); +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframer.html new file mode 100644 index 0000000000..0abfa55062 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html new file mode 100644 index 0000000000..188d78ee6e --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script src="shared_test_funcs.js"></script> +<script type="text/javascript"> +var popup; +async function runTheTest(iframe_domain, cross_origin_domain, mode) { + let s = `<!DOCTYPE html><html><script> + function give_result() { + return { + hardwareConcurrency : navigator.hardwareConcurrency + }; + } + window.addEventListener('load', async function listener(event) { + window.opener.postMessage(["popup_ready"], "*"); + }); + window.addEventListener('message', async function listener(event) { + if (event.data[0] == 'popup_is_ready') { + window.opener.postMessage(["popup_ready"], "*"); + } else if (event.data[0] == 'popup_request') { + let result = give_result(); + window.opener.postMessage(['popup_response', result], '*'); + } + });`; + // eslint-disable-next-line + s += `</` + `script></html>`; + + let params = new URLSearchParams(document.location.search); + let options = ""; + if (params.get("submode") == "noopener") { + options = "noopener"; + } + + let url = "data:text/html;charset=utf-8," + s; + popup = window.open(url, "", options); + + if (params.get("submode") == "noopener") { + return {}; + } + + await waitForMessage("popup_ready", `*`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + resolve(event.data[1]); + }, { once: true }); + }); + + popup.postMessage(["popup_request", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + popup.close(); + + return result; +} +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html new file mode 100644 index 0000000000..b66d97563c --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script> +var parent_window; +let params = new URLSearchParams(document.location.search); +if (params.get("mode") == "popup") { + parent_window = window.opener; +} else { + parent_window = window.parent; +} + +window.onload = async () => { + parent_window.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + let result = give_result(); + parent_window.postMessage(result, "*") + } +}); + +function give_result() { + return { + hardwareConcurrency : navigator.hardwareConcurrency + }; +} +</script> +<output id="result"></output> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html new file mode 100644 index 0000000000..3de74bc9a3 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain, mode) { + var child_reference; + let url = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html?mode=` + let params = new URLSearchParams(document.location.search); + + if (params.get("mode") == 'iframe') { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = url + 'iframe'; + child_reference = iframes[0].contentWindow; + } else if (params.get("mode") == "popup") { + let options = ""; + if (params.get("submode") == "noopener") { + options = "noopener"; + } + const popup = window.open(url + 'popup', '', options); + if (params.get("submode") == "noopener") { + return {}; + } + child_reference = popup; + } else { + throw new Error("Unknown page mode specified"); + } + + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + child_reference.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + if (params.get("mode") == "popup") { + child_reference.close(); + } + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html new file mode 100644 index 0000000000..8a4373c703 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<body> +<output id="result"></output> +<script type="text/javascript"> + window.addEventListener("load", function listener(event) { + parent.postMessage(["frame_ready"], "*"); + }); + window.addEventListener("message", function listener(event) { + if (event.data[0] == "gimme") { + let result = { + hardwareConcurrency : navigator.hardwareConcurrency + }; + + parent.postMessage(["frame_response", result], "*"); + } + }); +</script> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframee.html new file mode 100644 index 0000000000..3ae4453928 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframee.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script type="text/javascript"> +window.onload = () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", function listener(event) { + if (event.data[0] == "gimme") { + let iframe = document.createElement("iframe"); + iframe.src = "/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html"; + iframe.sandbox = "allow-scripts"; + document.body.append(iframe); + + } else if (event.data[0] == "frame_ready") { + let iframe = document.getElementsByTagName("iframe")[0]; + iframe.contentWindow.postMessage({0: "gimme"}, "*"); + } else if (event.data[0] == "frame_response") { + parent.postMessage(event.data[1], "*") + } +}); +</script> +<body> +<output id="result"></output> +</body> diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframer.html new file mode 100644 index 0000000000..60890c0260 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframer.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_keyBoardEvent.sjs b/browser/components/resistfingerprinting/test/browser/file_keyBoardEvent.sjs new file mode 100644 index 0000000000..5e3c0e2e57 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_keyBoardEvent.sjs @@ -0,0 +1,64 @@ +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +const HTML_DATA = ` + <!DOCTYPE HTML> + <html> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1222285</title> + <script type="application/javascript"> + function populateKeyEventResult(aEvent) { + let result = document.getElementById("result-" + aEvent.type); + let data = { + key: aEvent.key, + code: aEvent.code, + location: aEvent.location, + altKey: aEvent.altKey, + shiftKey: aEvent.shiftKey, + ctrlKey: aEvent.ctrlKey, + charCode: aEvent.charCode, + keyCode: aEvent.keyCode, + modifierState: { + Alt: aEvent.getModifierState("Alt"), + AltGraph: aEvent.getModifierState("AltGraph"), + Shift: aEvent.getModifierState("Shift"), + Control: aEvent.getModifierState("Control") + } + }; + + result.value = JSON.stringify(data); + result.dispatchEvent(new CustomEvent("resultAvailable")); + } + + window.onload = () => { + for (event of ["keydown", "keypress", "keyup"]) { + document.getElementById("test") + .addEventListener(event, populateKeyEventResult); + } + } + </script> + </head> + <body> + <input id="test"/> + <input id="result-keydown"/> + <input id="result-keypress"/> + <input id="result-keyup"/> + </body> + </html>`; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let queryString = new URLSearchParams(request.queryString); + + let language = queryString.get("language"); + + if (language) { + response.setHeader("Content-Language", language, false); + } + + response.write(HTML_DATA); +} diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator.html b/browser/components/resistfingerprinting/test/browser/file_navigator.html new file mode 100644 index 0000000000..73029a2168 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigator.html @@ -0,0 +1,33 @@ +<html> +<head> +<title>Test page for navigator object</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +<script> + // This page will collect information from the navigator object and store + // the result at a paragraph in the page. + function collect() { + let result = {}; + + result.appCodeName = navigator.appCodeName; + result.appName = navigator.appName; + result.appVersion = navigator.appVersion; + result.platform = navigator.platform; + result.userAgent = navigator.userAgent; + result.product = navigator.product; + result.productSub = navigator.productSub; + result.vendor = navigator.vendor; + result.vendorSub = navigator.vendorSub; + result.mimeTypesLength = navigator.mimeTypes.length; + result.pluginsLength = navigator.plugins.length; + result.oscpu = navigator.oscpu; + result.hardwareConcurrency = navigator.hardwareConcurrency; + + // eslint-disable-next-line no-unsanitized/property + document.getElementById("result").innerHTML = JSON.stringify(result); + } +</script> +</head> +<body onload="collect();"> +<p id="result"></p> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_navigatorWorker.js b/browser/components/resistfingerprinting/test/browser/file_navigatorWorker.js new file mode 100644 index 0000000000..b0a8e308ff --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigatorWorker.js @@ -0,0 +1,19 @@ +/* eslint-env worker */ + +onconnect = function (e) { + let port = e.ports[0]; + + let navigatorObj = self.navigator; + let result = {}; + + result.appCodeName = navigatorObj.appCodeName; + result.appName = navigatorObj.appName; + result.appVersion = navigatorObj.appVersion; + result.platform = navigatorObj.platform; + result.userAgent = navigatorObj.userAgent; + result.product = navigatorObj.product; + result.hardwareConcurrency = navigatorObj.hardwareConcurrency; + + port.postMessage(JSON.stringify(result)); + port.start(); +}; diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_header.sjs b/browser/components/resistfingerprinting/test/browser/file_navigator_header.sjs new file mode 100644 index 0000000000..0afa1f1b41 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigator_header.sjs @@ -0,0 +1,12 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + if (request.hasHeader("user-agent")) { + response.write(request.getHeader("user-agent")); + } else { + response.write("no user agent header"); + } +} diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_iframe_worker.sjs b/browser/components/resistfingerprinting/test/browser/file_navigator_iframe_worker.sjs new file mode 100644 index 0000000000..cbb54d06bc --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigator_iframe_worker.sjs @@ -0,0 +1,25 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/javascript"); + + let content = `onconnect = function(e) { + let port = e.ports[0]; + + let navigatorObj = self.navigator; + let result = {}; + + result.appCodeName = navigatorObj.appCodeName; + result.appName = navigatorObj.appName; + result.appVersion = navigatorObj.appVersion; + result.platform = navigatorObj.platform; + result.userAgent = navigatorObj.userAgent; + result.product = navigatorObj.product; + result.hardwareConcurrency = navigatorObj.hardwareConcurrency; + + port.postMessage(result); + port.start(); + };`; + + response.write(content); +} diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html new file mode 100644 index 0000000000..8e312d1d7b --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + let cross_origin_domain = event.data[1]; + var result = {}; + + result.appCodeName = navigator.appCodeName; + result.appName = navigator.appName; + result.appVersion = navigator.appVersion; + result.platform = navigator.platform; + result.userAgent = navigator.userAgent; + result.product = navigator.product; + result.productSub = navigator.productSub; + result.vendor = navigator.vendor; + result.vendorSub = navigator.vendorSub; + result.mimeTypesLength = navigator.mimeTypes.length; + result.pluginsLength = navigator.plugins.length; + result.oscpu = navigator.oscpu; + result.hardwareConcurrency = navigator.hardwareConcurrency; + result.userAgentHTTPHeader = "unknown"; + + let worker = new SharedWorker("file_navigator_iframe_worker.sjs"); + let worker_result = await new Promise(resolve => { + worker.port.onmessage = function(e) { + resolve(e.data); + }; + }); + + result.worker_appCodeName = worker_result.appCodeName; + result.worker_appName = worker_result.appName; + result.worker_appVersion = worker_result.appVersion; + result.worker_platform = worker_result.platform; + result.worker_userAgent = worker_result.userAgent; + result.worker_product = worker_result.product; + result.worker_hardwareConcurrency = worker_result.hardwareConcurrency; + + var one = fetch("file_navigator_header.sjs?") + .then((response) => { return response.text(); }) + .then((content) => { + result.userAgentHTTPHeader = content; + }); + + var two = fetch(`https://${cross_origin_domain}/browser/browser/components/resistfingerprinting/test/browser/file_navigator_header.sjs?`) + .then((response) => { return response.text(); }) + .then((content) => { + result.framee_crossOrigin_userAgentHTTPHeader = content; + }); + + Promise.all([one, two]).then((values) => { + parent.postMessage(result, "*") + }); + } +}); +</script> diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_iframer.html b/browser/components/resistfingerprinting/test/browser/file_navigator_iframer.html new file mode 100644 index 0000000000..3498393904 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_navigator_iframer.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + await fetch(`https://${cross_origin_domain}/browser/browser/components/resistfingerprinting/test/browser/file_navigator_header.sjs?`) + .then((response) => { return response.text(); }) + .then((content) => { + result.framer_crossOrigin_userAgentHTTPHeader = content; + }); + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframe_worker.sjs b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframe_worker.sjs new file mode 100644 index 0000000000..7791c05c06 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframe_worker.sjs @@ -0,0 +1,34 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/javascript"); + + let content = `onconnect = function(e) { + let port = e.ports[0]; + + let navigatorObj = self.navigator; + let result = []; + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + 'performance.now()', + 'new Date().getTime()', + 'new Event("").timeStamp', + 'new File([], "").lastModified', + ]; + + for (let timeStampCode of timeStampCodes) { + let timeStamp = eval(timeStampCode); + + result.push({ + 'name': 'worker ' + timeStampCode, + 'value': timeStamp + }); + } + + port.postMessage(result); + port.start(); + };`; + + response.write(content); +} diff --git a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html new file mode 100644 index 0000000000..d336fa8d85 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script> +window.onload = async () => { + parent.postMessage("ready", "*"); +} + +window.addEventListener("message", async function listener(event) { + if (event.data[0] == "gimme") { + var result = []; + + // Prepare for test of AudioContext.currentTime + // eslint-disable-next-line + let audioContext = new AudioContext(); + + // Known ways to generate time stamps, in milliseconds + const timeStampCodes = [ + "performance.now()", + "new Date().getTime()", + "new Event(\"\").timeStamp", + "new File([], \"\").lastModified", + ]; + // These are measured in seconds, so we need to scale them up + var timeStampCodesDOM = timeStampCodes.concat([ + "audioContext.currentTime * 1000", + ]); + + for (let timeStampCode of timeStampCodesDOM) { + // eslint-disable-next-line no-eval + let timeStamp = eval(timeStampCode); + + result.push({ + 'name': timeStampCode, + 'value': timeStamp + }); + } + + let worker = new SharedWorker("file_reduceTimePrecision_iframe_worker.sjs"); + let worker_result = await new Promise(resolve => { + worker.port.onmessage = function(e) { + resolve(e.data); + }; + }); + + for (let item of worker_result) { + result.push(item); + } + + parent.postMessage(result, "*") + } +}); +</script> diff --git a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html new file mode 100644 index 0000000000..4d9c81ec8d --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="shared_test_funcs.js"></script> +<script> +async function runTheTest(iframe_domain, cross_origin_domain, extraData) { + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html`; + await waitForMessage("ready", `https://${iframe_domain}`); + + const promiseForRFPTest = new Promise(resolve => { + window.addEventListener("message", event => { + if(event.origin != `https://${iframe_domain}`) { + throw new Error(`origin should be ${iframe_domain}`); + } + resolve(event.data); + }, { once: true }); + }); + iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*"); + var result = await promiseForRFPTest; + + return result; +} +</script> +</head> +<body> +<iframe width=100></iframe> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/browser/file_workerNetInfo.js b/browser/components/resistfingerprinting/test/browser/file_workerNetInfo.js new file mode 100644 index 0000000000..2d21fa6f5e --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_workerNetInfo.js @@ -0,0 +1,30 @@ +function ok(a, msg) { + postMessage({ type: "status", status: !!a, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage({ type: "finish" }); +} + +function runTests() { + ok("connection" in navigator, "navigator.connection should exist"); + is( + navigator.connection.type, + "unknown", + "The connection type is spoofed correctly" + ); + + finish(); +} + +self.onmessage = function (e) { + if (e.data.type === "runTests") { + runTests(); + } else { + ok(false, "Unknown message type"); + } +}; diff --git a/browser/components/resistfingerprinting/test/browser/file_workerPerformance.js b/browser/components/resistfingerprinting/test/browser/file_workerPerformance.js new file mode 100644 index 0000000000..4533073180 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/file_workerPerformance.js @@ -0,0 +1,122 @@ +function ok(a, msg) { + postMessage({ type: "status", status: !!a, msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage({ type: "finish" }); +} + +let isRounded = (x, expectedPrecision) => { + let rounded = Math.floor(x / expectedPrecision) * expectedPrecision; + // First we do the perfectly normal check that should work just fine + if (rounded === x || x === 0) { + return true; + } + + // When we're diving by non-whole numbers, we may not get perfect + // multiplication/division because of floating points. + // When dealing with ms since epoch, a double's precision is on the order + // of 1/5 of a microsecond, so we use a value a little higher than that as + // our epsilon. + // To be clear, this error is introduced in our re-calculation of 'rounded' + // above in JavaScript. + if (Math.abs(rounded - x + expectedPrecision) < 0.0005) { + return true; + } else if (Math.abs(rounded - x) < 0.0005) { + return true; + } + + // Then we handle the case where you're sub-millisecond and the timer is not + // We check that the timer is not sub-millisecond by assuming it is not if it + // returns an even number of milliseconds + if (expectedPrecision < 1 && Math.round(x) == x) { + if (Math.round(rounded) == x) { + return true; + } + } + + ok( + false, + "Looming Test Failure, Additional Debugging Info: Expected Precision: " + + expectedPrecision + + " Measured Value: " + + x + + " Rounded Vaue: " + + rounded + + " Fuzzy1: " + + Math.abs(rounded - x + expectedPrecision) + + " Fuzzy 2: " + + Math.abs(rounded - x) + ); + + return false; +}; + +function runTimerTests(expectedPrecision) { + ok( + isRounded(performance.timeOrigin, expectedPrecision), + `In a worker, for reduceTimerPrecision, performance.timeOrigin is not correctly rounded: ` + + performance.timeOrigin + ); + + // Try to add some entries. + performance.mark("Test"); + performance.mark("Test-End"); + performance.measure("Test-Measure", "Test", "Test-End"); + + // Check the entries in performance.getEntries/getEntriesByType/getEntriesByName. + is( + performance.getEntries().length, + 3, + "In a worker, for reduceTimerPrecision: Incorrect number of entries for performance.getEntries() for workers: " + + performance.getEntries().length + ); + for (var i = 0; i < 3; i++) { + let startTime = performance.getEntries()[i].startTime; + let duration = performance.getEntries()[i].duration; + ok( + isRounded(startTime, expectedPrecision), + "In a worker, for reduceTimerPrecision(" + + expectedPrecision + + "), performance.getEntries(" + + i.toString() + + ").startTime is not rounded: " + + startTime.toString() + ); + ok( + isRounded(duration, expectedPrecision), + "In a worker, for reduceTimerPrecision(" + + expectedPrecision + + "), performance.getEntries(" + + i.toString() + + ").duration is not rounded: " + + duration.toString() + ); + } + is( + performance.getEntriesByType("mark").length, + 2, + "In a worker, for reduceTimerPrecision: Incorrect number of entries for performance.getEntriesByType() for workers: " + + performance.getEntriesByType("resource").length + ); + is( + performance.getEntriesByName("Test", "mark").length, + 1, + "In a worker, for reduceTimerPrecision: Incorrect number of entries for performance.getEntriesByName() for workers: " + + performance.getEntriesByName("Test", "mark").length + ); + + finish(); +} + +self.onmessage = function (e) { + if (e.data.type === "runTimerTests") { + runTimerTests(e.data.precision); + } else { + ok(false, "Unknown message type"); + } +}; diff --git a/browser/components/resistfingerprinting/test/browser/head.js b/browser/components/resistfingerprinting/test/browser/head.js new file mode 100644 index 0000000000..244bd578ef --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/head.js @@ -0,0 +1,1063 @@ +/* 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ContentBlockingAllowList: + "resource://gre/modules/ContentBlockingAllowList.sys.mjs", +}); + +const TEST_PATH = + "http://example.net/browser/browser/" + + "components/resistfingerprinting/test/browser/"; + +const PERFORMANCE_TIMINGS = [ + "navigationStart", + "unloadEventStart", + "unloadEventEnd", + "redirectStart", + "redirectEnd", + "fetchStart", + "domainLookupStart", + "domainLookupEnd", + "connectStart", + "connectEnd", + "secureConnectionStart", + "requestStart", + "responseStart", + "responseEnd", + "domLoading", + "domInteractive", + "domContentLoadedEventStart", + "domContentLoadedEventEnd", + "domComplete", + "loadEventStart", + "loadEventEnd", +]; + +/** + * Sets up tests for making sure that performance APIs have been correctly + * spoofed or disabled. + */ +let setupPerformanceAPISpoofAndDisableTest = async function ( + resistFingerprinting, + reduceTimerPrecision, + crossOriginIsolated, + expectedPrecision, + runTests, + workerCall +) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", resistFingerprinting], + ["privacy.reduceTimerPrecision", reduceTimerPrecision], + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + expectedPrecision * 1000, + ], + ["browser.tabs.remote.useCrossOriginOpenerPolicy", crossOriginIsolated], + ["browser.tabs.remote.useCrossOriginEmbedderPolicy", crossOriginIsolated], + ], + }); + + let url = crossOriginIsolated + ? `https://example.com/browser/browser/components/resistfingerprinting` + + `/test/browser/coop_header.sjs?crossOriginIsolated=${crossOriginIsolated}` + : TEST_PATH + "file_dummy.html"; + + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); + + // No matter what we set the precision to, if we're in ResistFingerprinting + // mode we use the larger of the precision pref and the RFP time-atom constant + if (resistFingerprinting) { + const RFP_TIME_ATOM_MS = 16.667; + expectedPrecision = Math.max(RFP_TIME_ATOM_MS, expectedPrecision); + } + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + list: PERFORMANCE_TIMINGS, + resistFingerprinting, + precision: expectedPrecision, + isRoundedFunc: isTimeValueRounded.toString(), + workerCall, + }, + ], + runTests + ); + + if (crossOriginIsolated) { + let remoteType = tab.linkedBrowser.remoteType; + ok( + remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX), + `${remoteType} expected to be coop+coep` + ); + } + + await BrowserTestUtils.closeWindow(win); +}; + +let isTimeValueRounded = (x, expectedPrecision, console) => { + const nearestExpected = Math.round(x / expectedPrecision) * expectedPrecision; + // First we do the perfectly normal check that should work just fine + if (x === nearestExpected) { + return true; + } + + // When we're dividing by non-whole numbers, we may not get perfect + // multiplication/division because of floating points. + // When dealing with ms since epoch, a double's precision is on the order + // of 1/5 of a microsecond, so we use a value a little higher than that as + // our epsilon. + // To be clear, this error is introduced in our re-calculation of 'rounded' + // above in JavaScript. + const error = Math.abs(x - nearestExpected); + + if (console) { + console.log( + "Additional Debugging Info: Expected Precision: " + + expectedPrecision + + " Measured Value: " + + x + + " Nearest Expected Vaue: " + + nearestExpected + + " Error: " + + error + ); + } + + if (Math.abs(error) < 0.0005) { + return true; + } + + // Then we handle the case where you're sub-millisecond and the timer is not + // We check that the timer is not sub-millisecond by assuming it is not if it + // returns an even number of milliseconds + if ( + Math.round(expectedPrecision) != expectedPrecision && + Math.round(x) == x + ) { + let acceptableIntRounding = false; + acceptableIntRounding |= Math.floor(nearestExpected) == x; + acceptableIntRounding |= Math.ceil(nearestExpected) == x; + if (acceptableIntRounding) { + return true; + } + } + + return false; +}; + +let setupAndRunCrossOriginIsolatedTest = async function ( + options, + expectedPrecision, + runTests, + workerCall +) { + let prefsToSet = [ + [ + "privacy.resistFingerprinting.reduceTimerPrecision.microseconds", + expectedPrecision * 1000, + ], + ]; + + if (options.resistFingerprinting !== undefined) { + prefsToSet = prefsToSet.concat([ + ["privacy.resistFingerprinting", options.resistFingerprinting], + ]); + } else { + options.resistFingerprinting = false; + } + + if (options.resistFingerprintingPBMOnly !== undefined) { + prefsToSet = prefsToSet.concat([ + [ + "privacy.resistFingerprinting.pbmode", + options.resistFingerprintingPBMOnly, + ], + ]); + } else { + options.resistFingerprintingPBMOnly = false; + } + + if (options.reduceTimerPrecision !== undefined) { + prefsToSet = prefsToSet.concat([ + ["privacy.reduceTimerPrecision", options.reduceTimerPrecision], + ]); + } else { + options.reduceTimerPrecision = false; + } + + if (options.crossOriginIsolated !== undefined) { + prefsToSet = prefsToSet.concat([ + [ + "browser.tabs.remote.useCrossOriginOpenerPolicy", + options.crossOriginIsolated, + ], + [ + "browser.tabs.remote.useCrossOriginEmbedderPolicy", + options.crossOriginIsolated, + ], + ]); + } else { + options.crossOriginIsolated = false; + } + + if (options.openPrivateWindow === undefined) { + options.openPrivateWindow = false; + } + if (options.shouldBeRounded === undefined) { + options.shouldBeRounded = true; + } + + console.log(prefsToSet); + await SpecialPowers.pushPrefEnv({ set: prefsToSet }); + + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: options.openPrivateWindow, + }); + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + `https://example.com/browser/browser/components/resistfingerprinting` + + `/test/browser/coop_header.sjs?crossOriginIsolated=${options.crossOriginIsolated}` + ); + + // No matter what we set the precision to, if we're in ResistFingerprinting + // mode we use the larger of the precision pref and the RFP time-atom constant + if ( + options.resistFingerprinting || + (options.resistFingerprintingPBMOnly && options.openPrivateWindow) + ) { + const RFP_TIME_ATOM_MS = 16.667; + expectedPrecision = Math.max(RFP_TIME_ATOM_MS, expectedPrecision); + } + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + precision: expectedPrecision, + isRoundedFunc: isTimeValueRounded.toString(), + workerCall, + options, + }, + ], + runTests + ); + + if (options.crossOriginIsolated) { + let remoteType = tab.linkedBrowser.remoteType; + ok( + remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX), + `${remoteType} expected to be coop+coep` + ); + } + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}; + +// This function calculates the maximum available window dimensions and returns +// them as an object. +async function calcMaximumAvailSize(aChromeWidth, aChromeHeight) { + let chromeUIWidth; + let chromeUIHeight; + let testPath = + "http://example.net/browser/browser/" + + "components/resistfingerprinting/test/browser/"; + + // If the chrome UI dimensions is not given, we will calculate it. + if (!aChromeWidth || !aChromeHeight) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + testPath + "file_dummy.html" + ); + + let contentSize = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + let result = { + width: content.innerWidth, + height: content.innerHeight, + }; + + return result; + } + ); + + // Calculate the maximum available window size which is depending on the + // available screen space. + chromeUIWidth = win.outerWidth - contentSize.width; + chromeUIHeight = win.outerHeight - contentSize.height; + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + } else { + chromeUIWidth = aChromeWidth; + chromeUIHeight = aChromeHeight; + } + + let availWidth = window.screen.availWidth; + let availHeight = window.screen.availHeight; + + // Ideally, we would round the window size as 1000x1000. But the available + // screen space might not suffice. So, we decide the size according to the + // available screen size. + let availContentWidth = Math.min(1000, availWidth - chromeUIWidth); + let availContentHeight; + + // If it is GTK window, we would consider the system decorations when we + // calculating avail content height since the system decorations won't be + // reported when we get available screen dimensions. + if (AppConstants.MOZ_WIDGET_GTK) { + availContentHeight = Math.min(1000, -40 + availHeight - chromeUIHeight); + } else { + availContentHeight = Math.min(1000, availHeight - chromeUIHeight); + } + + // Rounded the desire size to the nearest 200x100. + let maxAvailWidth = availContentWidth - (availContentWidth % 200); + let maxAvailHeight = availContentHeight - (availContentHeight % 100); + + return { maxAvailWidth, maxAvailHeight }; +} + +async function calcPopUpWindowChromeUISize() { + let testPath = + "http://example.net/browser/browser/" + + "components/resistFingerprinting/test/browser/"; + // open a popup window to acquire the chrome UI size of it. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + testPath + "file_dummy.html" + ); + + let result = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + let win; + + await new Promise(resolve => { + win = content.open("about:blank", "", "width=1000,height=1000"); + win.onload = () => resolve(); + }); + + let res = { + chromeWidth: win.outerWidth - win.innerWidth, + chromeHeight: win.outerHeight - win.innerHeight, + }; + + win.close(); + + return res; + } + ); + + BrowserTestUtils.removeTab(tab); + + return result; +} + +async function testWindowOpen( + aBrowser, + aSettingWidth, + aSettingHeight, + aTargetWidth, + aTargetHeight, + aMaxAvailWidth, + aMaxAvailHeight, + aPopupChromeUIWidth, + aPopupChromeUIHeight +) { + // If the target size is greater than the maximum available content size, + // we set the target size to it. + if (aTargetWidth > aMaxAvailWidth) { + aTargetWidth = aMaxAvailWidth; + } + + if (aTargetHeight > aMaxAvailHeight) { + aTargetHeight = aMaxAvailHeight; + } + + // Create the testing window features. + let winFeatures = "width=" + aSettingWidth + ",height=" + aSettingHeight; + + let testParams = { + winFeatures, + targetWidth: aTargetWidth, + targetHeight: aTargetHeight, + }; + + await SpecialPowers.spawn(aBrowser, [testParams], async function (input) { + // Call window.open() with window features. + await new Promise(resolve => { + let win = content.open("http://example.net/", "", input.winFeatures); + + win.onload = () => { + is( + win.screen.width, + input.targetWidth, + "The screen.width has a correct rounded value" + ); + is( + win.screen.height, + input.targetHeight, + "The screen.height has a correct rounded value" + ); + is( + win.innerWidth, + input.targetWidth, + "The window.innerWidth has a correct rounded value" + ); + is( + win.innerHeight, + input.targetHeight, + "The window.innerHeight has a correct rounded value" + ); + + win.close(); + resolve(); + }; + }); + }); +} + +async function testWindowSizeSetting( + aBrowser, + aSettingWidth, + aSettingHeight, + aTargetWidth, + aTargetHeight, + aInitWidth, + aInitHeight, + aTestOuter, + aMaxAvailWidth, + aMaxAvailHeight, + aPopupChromeUIWidth, + aPopupChromeUIHeight +) { + // If the target size is greater than the maximum available content size, + // we set the target size to it. + if (aTargetWidth > aMaxAvailWidth) { + aTargetWidth = aMaxAvailWidth; + } + + if (aTargetHeight > aMaxAvailHeight) { + aTargetHeight = aMaxAvailHeight; + } + + let testParams = { + initWidth: aInitWidth, + initHeight: aInitHeight, + settingWidth: aSettingWidth + (aTestOuter ? aPopupChromeUIWidth : 0), + settingHeight: aSettingHeight + (aTestOuter ? aPopupChromeUIHeight : 0), + targetWidth: aTargetWidth, + targetHeight: aTargetHeight, + testOuter: aTestOuter, + }; + + await SpecialPowers.spawn(aBrowser, [testParams], async function (input) { + let win; + // Open a new window and wait until it loads. + await new Promise(resolve => { + // Given a initial window size which should be different from target + // size. We need this to trigger 'onresize' event. + let initWinFeatures = + "width=" + input.initWidth + ",height=" + input.initHeight; + win = content.open("http://example.net/", "", initWinFeatures); + win.onload = () => resolve(); + }); + + // Test inner/outerWidth. + await new Promise(resolve => { + win.addEventListener( + "resize", + () => { + is( + win.screen.width, + input.targetWidth, + "The screen.width has a correct rounded value" + ); + is( + win.innerWidth, + input.targetWidth, + "The window.innerWidth has a correct rounded value" + ); + + resolve(); + }, + { once: true } + ); + + if (input.testOuter) { + win.outerWidth = input.settingWidth; + } else { + win.innerWidth = input.settingWidth; + } + }); + + win.close(); + // Open a new window and wait until it loads. + await new Promise(resolve => { + // Given a initial window size which should be different from target + // size. We need this to trigger 'onresize' event. + let initWinFeatures = + "width=" + input.initWidth + ",height=" + input.initHeight; + win = content.open("http://example.net/", "", initWinFeatures); + win.onload = () => resolve(); + }); + + // Test inner/outerHeight. + await new Promise(resolve => { + win.addEventListener( + "resize", + () => { + is( + win.screen.height, + input.targetHeight, + "The screen.height has a correct rounded value" + ); + is( + win.innerHeight, + input.targetHeight, + "The window.innerHeight has a correct rounded value" + ); + + resolve(); + }, + { once: true } + ); + + if (input.testOuter) { + win.outerHeight = input.settingHeight; + } else { + win.innerHeight = input.settingHeight; + } + }); + + win.close(); + }); +} + +class RoundedWindowTest { + // testOuter is optional. run() can be invoked with only 1 parameter. + static run(testCases, testOuter) { + // "this" is the calling class itself. + // e.g. when invoked by RoundedWindowTest.run(), "this" is "class RoundedWindowTest". + let test = new this(testCases); + add_task(async () => test.setup()); + add_task(async () => { + if (testOuter == undefined) { + // If testOuter is not given, do tests for both inner and outer. + await test.doTests(false); + await test.doTests(true); + } else { + await test.doTests(testOuter); + } + }); + } + + constructor(testCases) { + this.testCases = testCases; + } + + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]], + }); + + // Calculate the popup window's chrome UI size for tests of outerWidth/Height. + let popUpChromeUISize = await calcPopUpWindowChromeUISize(); + + this.popupChromeUIWidth = popUpChromeUISize.chromeWidth; + this.popupChromeUIHeight = popUpChromeUISize.chromeHeight; + + // Calculate the maximum available size. + let maxAvailSize = await calcMaximumAvailSize( + this.popupChromeUIWidth, + this.popupChromeUIHeight + ); + + this.maxAvailWidth = maxAvailSize.maxAvailWidth; + this.maxAvailHeight = maxAvailSize.maxAvailHeight; + } + + async doTests(testOuter) { + // Open a tab to test. + this.tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_dummy.html" + ); + + for (let test of this.testCases) { + await this.doTest(test, testOuter); + } + + BrowserTestUtils.removeTab(this.tab); + } + + async doTest() { + throw new Error("RoundedWindowTest.doTest must be overridden."); + } +} + +class WindowSettingTest extends RoundedWindowTest { + async doTest(test, testOuter) { + await testWindowSizeSetting( + this.tab.linkedBrowser, + test.settingWidth, + test.settingHeight, + test.targetWidth, + test.targetHeight, + test.initWidth, + test.initHeight, + testOuter, + this.maxAvailWidth, + this.maxAvailHeight, + this.popupChromeUIWidth, + this.popupChromeUIHeight + ); + } +} + +class OpenTest extends RoundedWindowTest { + async doTest(test) { + await testWindowOpen( + this.tab.linkedBrowser, + test.settingWidth, + test.settingHeight, + test.targetWidth, + test.targetHeight, + this.maxAvailWidth, + this.maxAvailHeight, + this.popupChromeUIWidth, + this.popupChromeUIHeight + ); + } +} + +// ============================================================ +const FRAMER_DOMAIN = "example.com"; +const IFRAME_DOMAIN = "example.org"; +const CROSS_ORIGIN_DOMAIN = "example.net"; + +async function runActualTest(uri, testFunction, expectedResults, extraData) { + let browserWin = gBrowser; + let openedWin = null; + + if ("private_window" in extraData) { + openedWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + browserWin = openedWin.gBrowser; + } + + let tab = await BrowserTestUtils.openNewForegroundTab(browserWin, uri); + + if ("etp_reload" in extraData) { + ContentBlockingAllowList.add(tab.linkedBrowser); + await BrowserTestUtils.reloadTab(tab); + } + + /* + * We expect that `runTheTest` is going to be able to communicate with the iframe + * or tab that it opens, but if it cannot (because we are using noopener), we kind + * of hack around and get the data directly. + */ + if ("noopener" in extraData) { + var popupTabPromise = BrowserTestUtils.waitForNewTab( + browserWin, + extraData.await_uri + ); + } + + // In SpecialPowers.spawn, extraData goes through a structuredClone, which cannot clone + // functions. await_uri is sometimes a function. This filters out keys that are used by + // this function (runActualTest) and not by runTheTest or testFunction. It avoids the + // cloning issue, and avoids polluting the object in those called functions. + let filterExtraData = function (x) { + let banned_keys = ["private_window", "etp_reload", "noopener", "await_uri"]; + return Object.fromEntries( + Object.entries(x).filter(([k, v]) => !banned_keys.includes(k)) + ); + }; + + let result = await SpecialPowers.spawn( + tab.linkedBrowser, + [IFRAME_DOMAIN, CROSS_ORIGIN_DOMAIN, filterExtraData(extraData)], + async function (iframe_domain_, cross_origin_domain_, extraData_) { + return content.wrappedJSObject.runTheTest( + iframe_domain_, + cross_origin_domain_, + extraData_ + ); + } + ); + + if ("noopener" in extraData) { + await popupTabPromise; + if (Services.appinfo.OS === "WINNT") { + await new Promise(r => setTimeout(r, 1000)); + } + + let popup_tab = browserWin.tabs[browserWin.tabs.length - 1]; + result = await SpecialPowers.spawn( + popup_tab.linkedBrowser, + [], + async function () { + let r = content.wrappedJSObject.give_result(); + return r; + } + ); + BrowserTestUtils.removeTab(popup_tab); + } + + testFunction(result, expectedResults, extraData); + + if ("etp_reload" in extraData) { + ContentBlockingAllowList.remove(tab.linkedBrowser); + } + BrowserTestUtils.removeTab(tab); + if ("private_window" in extraData) { + await BrowserTestUtils.closeWindow(openedWin); + } +} + +async function defaultsTest( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "default"; + expectedResults.shouldRFPApply = false; + if (extraPrefs != undefined) { + await SpecialPowers.pushPrefEnv({ + set: extraPrefs, + }); + } + await runActualTest(uri, testFunction, expectedResults, extraData); + if (extraPrefs != undefined) { + await SpecialPowers.popPrefEnv(); + } +} + +async function simpleRFPTest( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "simple RFP enabled"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting", true]].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +async function simplePBMRFPTest( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.private_window = true; + extraData.testDesc = extraData.testDesc || "simple RFP in PBM enabled"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [["privacy.resistFingerprinting.pbmode", true]].concat( + extraPrefs || [] + ), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +async function simpleFPPTest( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "simple FPP enabled"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +async function simplePBMFPPTest( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.private_window = true; + extraData.testDesc = extraData.testDesc || "simple FPP in PBM enabled"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.fingerprintingProtection.pbmode", true], + ["privacy.fingerprintingProtection.overrides", "+HardwareConcurrency"], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain +async function testA( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (A)"; + expectedResults.shouldRFPApply = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + `${FRAMER_DOMAIN}, ${IFRAME_DOMAIN}, ${CROSS_ORIGIN_DOMAIN}`, + ], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain +async function testB( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (B)"; + expectedResults.shouldRFPApply = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + `${FRAMER_DOMAIN}, ${IFRAME_DOMAIN}`, + ], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee +async function testC( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (C)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + `${FRAMER_DOMAIN}, ${CROSS_ORIGIN_DOMAIN}`, + ], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain +async function testD( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (D)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", `${FRAMER_DOMAIN}`], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain +async function testE( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (E)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + `${CROSS_ORIGIN_DOMAIN}`, + ], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain +async function testF( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (F)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", ""], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain +async function testG( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (G)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + [ + "privacy.resistFingerprinting.exemptedDomains", + `${IFRAME_DOMAIN}, ${CROSS_ORIGIN_DOMAIN}`, + ], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} + +// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee +async function testH( + uri, + testFunction, + expectedResults, + extraData, + extraPrefs +) { + if (extraData == undefined) { + extraData = {}; + } + extraData.testDesc = extraData.testDesc || "test (H)"; + expectedResults.shouldRFPApply = true; + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", `${IFRAME_DOMAIN}`], + ].concat(extraPrefs || []), + }); + + await runActualTest(uri, testFunction, expectedResults, extraData); + + await SpecialPowers.popPrefEnv(); +} diff --git a/browser/components/resistfingerprinting/test/browser/shared_test_funcs.js b/browser/components/resistfingerprinting/test/browser/shared_test_funcs.js new file mode 100644 index 0000000000..0949ef5848 --- /dev/null +++ b/browser/components/resistfingerprinting/test/browser/shared_test_funcs.js @@ -0,0 +1,10 @@ +function waitForMessage(aMsg, aOrigin) { + return new Promise(resolve => { + window.addEventListener("message", function listener(event) { + if (event.data == aMsg && (aOrigin == "*" || event.origin == aOrigin)) { + window.removeEventListener("message", listener); + resolve(); + } + }); + }); +} diff --git a/browser/components/resistfingerprinting/test/mochitest/.eslintrc.js b/browser/components/resistfingerprinting/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..16ee78885f --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + rules: { + "no-eval": "off", + }, +}; diff --git a/browser/components/resistfingerprinting/test/mochitest/decode_error.mp4 b/browser/components/resistfingerprinting/test/mochitest/decode_error.mp4 Binary files differnew file mode 100644 index 0000000000..ee72e2dd75 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/decode_error.mp4 diff --git a/browser/components/resistfingerprinting/test/mochitest/file_animation_api.html b/browser/components/resistfingerprinting/test/mochitest/file_animation_api.html new file mode 100644 index 0000000000..376fddf343 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/file_animation_api.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1382545</title> +<script> + function waitForCondition(aCond, aCallback, aErrorMsg) { + var tries = 0; + var interval = setInterval(() => { + if (tries >= 30) { + opener.ok(false, aErrorMsg); + moveOn(); + return; + } + var conditionPassed; + try { + conditionPassed = aCond(); + } catch (e) { + opener.ok(false, `${e}\n${e.stack}`); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = () => { clearInterval(interval); aCallback(); }; + } + + function runTest() { + let expectedPrecision = opener.expectedPrecision / 1000; + let isRounded = (x) => { + let rounded = (Math.floor(x / expectedPrecision) * expectedPrecision); + // First we do the perfectly normal check that should work just fine + if (rounded === x || x === 0) + return true; + + // When we're diving by non-whole numbers, we may not get perfect + // multiplication/division because of floating points. + // When dealing with ms since epoch, a double's precision is on the order + // of 1/5 of a microsecond, so we use a value a little higher than that as + // our epsilon. + // To be clear, this error is introduced in our re-calculation of 'rounded' + // above in JavaScript. + if (Math.abs(rounded - x + expectedPrecision) < .0005) { + return true; + } else if (Math.abs(rounded - x) < .0005) { + return true; + } + + // Then we handle the case where you're sub-millisecond and the timer is not + // We check that the timer is not sub-millisecond by assuming it is not if it + // returns an even number of milliseconds + if (expectedPrecision < 1 && Math.round(x) == x) { + if (Math.round(rounded) == x) { + return true; + } + } + + // We are temporarily disabling this extra debugging failure because we expect to return false in some instances + // When we correct things we will re-enable it for debugging assistance + // opener.ok(false, "Looming Test Failure, Additional Debugging Info: Expected Precision: " + expectedPrecision + " Measured Value: " + x + + // " Rounded Vaue: " + rounded + " Fuzzy1: " + Math.abs(rounded - x + expectedPrecision) + + // " Fuzzy 2: " + Math.abs(rounded - x)); + + return false; + }; + const testDiv = document.getElementById("testDiv"); + const animation = testDiv.animate({ opacity: [0, 1] }, 100000); + animation.play(); + + waitForCondition( + () => animation.currentTime > 100, + () => { + // We have disabled Time Precision Reduction for CSS Animations, so we expect those tests to fail. + // If we are testing that preference, we accept either rounded or not rounded values as A-OK. + var maybeAcceptEverything = function(value) { + if (opener.prefName.includes("privacy.reduceTimerPrecision") && + !opener.prefName.includes("privacy.resistFingerprinting")) + return true; + return value; + }; + + opener.ok(maybeAcceptEverything(isRounded(animation.startTime)), + "pref: " + opener.prefName + " - animation.startTime with precision " + expectedPrecision + " is not rounded: " + animation.startTime); + opener.ok(maybeAcceptEverything(isRounded(animation.currentTime)), + "pref: " + opener.prefName + " - animation.currentTime with precision " + expectedPrecision + " is not rounded: " + animation.currentTime); + opener.ok(maybeAcceptEverything(isRounded(animation.timeline.currentTime)), + "pref: " + opener.prefName + " - animation.timeline.currentTime with precision " + expectedPrecision + " is not rounded: " + animation.timeline.currentTime); + if (document.timeline) { + opener.ok(maybeAcceptEverything(isRounded(document.timeline.currentTime)), + "pref: " + opener.prefName + " - document.timeline.currentTime with precision " + expectedPrecision + " is not rounded: " + document.timeline.currentTime); + } + opener.done(); + window.close(); + }, + "animation failed to start"); + } +</script> +</head> +<body onload="runTest();"> +<div id="testDiv">test</div> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/mochitest.ini b/browser/components/resistfingerprinting/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..2429f5e4dc --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/mochitest.ini @@ -0,0 +1,29 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +tags = resistfingerprinting + +support-files = + file_animation_api.html + worker_child.js + worker_grandchild.js + !/dom/tests/mochitest/geolocation/network_geolocation.sjs + +[test_animation_api.html] +[test_bug1354633_media_error.html] +support-files = decode_error.mp4 +[test_bug1382499_touch_api.html] +[test_bug863246_resource_uri.html] +[test_device_sensor_event.html] +[test_geolocation.html] +scheme = https +fail-if = xorigin +[test_hide_gamepad_info.html] +scheme = https +support-files = test_hide_gamepad_info_iframe.html +[test_iframe.html] +[test_keyboard_event.html] +[test_pointer_event.html] + support-files = + ../../../../../dom/events/test/pointerevents/mochitest_support_external.js +[test_speech_synthesis.html] +skip-if = verify diff --git a/browser/components/resistfingerprinting/test/mochitest/test_animation_api.html b/browser/components/resistfingerprinting/test/mochitest/test_animation_api.html new file mode 100644 index 0000000000..e754eeac7d --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_animation_api.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1382545 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1382545</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1382545 */ + SimpleTest.waitForExplicitFinish(); + + // Used by file_animation_api.html + var prefName = ""; + var expectedPrecision = 0; + var resistFingerprinting = false; + var reduceTimerPrecision = false; + + function runTest() { + // No matter what we set the precision to, if we're in ResistFingerprinting mode + // we use the larger of the precision pref and the constant RFP time-atom + if (resistFingerprinting) { + const RFP_TIME_ATOM_MS = 16.667; + expectedPrecision = Math.max(1000*RFP_TIME_ATOM_MS, expectedPrecision); + } + window.open("file_animation_api.html"); + } + + function setupTest(rfp, rtp, ep) { + // Set globals + expectedPrecision = ep; + resistFingerprinting = rfp; + reduceTimerPrecision = rtp; + prefName = ""; + prefName += resistFingerprinting ? "privacy.resistFingerprinting " : ""; + prefName += reduceTimerPrecision ? "privacy.reduceTimerPrecision " : ""; + SpecialPowers.pushPrefEnv({"set": + [ + ["dom.animations-api.timelines.enabled", true], + ["privacy.resistFingerprinting", resistFingerprinting], + ["privacy.reduceTimerPrecision", reduceTimerPrecision], + ["privacy.resistFingerprinting.reduceTimerPrecision.microseconds", expectedPrecision], + ], + }, runTest); + } + + var testIndx = 0; + var testSequence = [ + [true, false, 100000], + [false, true, 100000], + [true, false, 50000], + [false, true, 50000], + [true, false, 100], + [false, true, 100], + [true, true, 13], + [false, true, 13], + ]; + + window.onload = () => { + setupTest(testSequence[testIndx][0], testSequence[testIndx][1], testSequence[testIndx][2]); + }; + + function done() { + testIndx++; + if (testIndx == testSequence.length) { + SimpleTest.finish(); + } else { + setupTest(testSequence[testIndx][0], testSequence[testIndx][1], testSequence[testIndx][2]); + } + } + </script> +</head> +<body> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_bug1354633_media_error.html b/browser/components/resistfingerprinting/test/mochitest/test_bug1354633_media_error.html new file mode 100644 index 0000000000..1155ab1778 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_bug1354633_media_error.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +/* global SimpleTest SpecialPowers */ + +let errorMessageMap = {}; + +let testPromise = (resistFingerprinting, src, allowlist) => new Promise(resolve => { + let video = document.createElement("video"); + video.src = src; + video.controls = "true"; + video.onerror = () => { + let message = video.error.message; + if (!resistFingerprinting) { + SimpleTest.isnot(message, "", "Message should not be blank"); + SimpleTest.info(src + ": " + message); + errorMessageMap[src] = message; + } else if (allowlist) { + SimpleTest.is(message, allowlist, "Error message in allowlist: " + allowlist); + } else { + SimpleTest.is(message, "", "Blank error message: " + errorMessageMap[src]); + } + resolve(); + }; + document.body.appendChild(video); +}); + +async function testBody(resistFingerprinting) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", resistFingerprinting], + ], + }); + await testPromise( + resistFingerprinting, + "load_error.mp4", + "404: Not Found" // allowlist + ); + await testPromise( + resistFingerprinting, + "decode_error.mp4", + false // allowlist + ); +} + +SimpleTest.waitForExplicitFinish(); +document.addEventListener("DOMContentLoaded", async () => { + await testBody(false); + await testBody(true); + SimpleTest.finish(); +}); +</script> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_bug1382499_touch_api.html b/browser/components/resistfingerprinting/test/mochitest/test_bug1382499_touch_api.html new file mode 100644 index 0000000000..c440d37cf2 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_bug1382499_touch_api.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script> +/* global SimpleTest SpecialPowers synthesizeTouch */ + +SimpleTest.waitForExplicitFinish(); + +function promiseEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, resolve, {once: true}); + }); +} + +function promiseTouchEvent(target, type, offsetX, offsetY, params) { + let touchEventPromise = promiseEvent(target, type); + params.type = type; + synthesizeTouch(target, offsetX, offsetY, params); + return touchEventPromise; +} + +document.addEventListener("DOMContentLoaded", async () => { + const target0 = document.getElementById("target0"); + const touchParams = {force: 1.0, angle: 1.0, rx: 2, ry: 3}; + await SpecialPowers.pushPrefEnv({set: [["dom.w3c_touch_events.enabled", 1]]}); + + for (let seq of [0, 1, 2, 3]) { + let resist = false; + if (seq == 0) { + resist = true; + await SpecialPowers.pushPrefEnv({set: [["privacy.resistFingerprinting", true]]}); + } else if (seq == 1) { + await SpecialPowers.pushPrefEnv({set: [["privacy.resistFingerprinting", false]]}); + } else if (seq == 2) { + resist = true; + await SpecialPowers.pushPrefEnv({set: [ + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "+TouchEvents"] + ]}); + } else { + await SpecialPowers.pushPrefEnv({set: [ + ["privacy.fingerprintingProtection", true], + ["privacy.fingerprintingProtection.overrides", "-TouchEvents"] + ]}); + } + + info("starting test with fingerprinting resistance " + (resist ? "on" : "off") + " sequence number " + seq); + let touchEvent = await promiseTouchEvent(target0, "touchstart", 5, 5, touchParams); + info("touch event received"); + let touch = touchEvent.touches[0]; + + if (resist) { + is(touch.screenX, touch.clientX, "touch.screenX should be the same as touch.clientX"); + is(touch.screenY, touch.clientY, "touch.screenY should be the same as touch.clientY"); + // radiusX/radiusY may differ from the original rx/ry because of AppUnitsPerCSSPixel and AppUnitsPerDevPixel. + // So only check if the values are spoofed. + is(touch.radiusX, 0, "touch.radiusX"); + is(touch.radiusY, 0, "touch.radiusY"); + } + is(touch.force, resist ? 0.0 : touchParams.force, "touch.force"); + is(touch.rotationAngle, resist ? 0 : touchParams.angle, "touch.rotationAngle"); + await SpecialPowers.popPrefEnv(); + } + + SimpleTest.finish(); +}); +</script> +<div id="target0">target 0</div> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_bug863246_resource_uri.html b/browser/components/resistfingerprinting/test/mochitest/test_bug863246_resource_uri.html new file mode 100644 index 0000000000..fda1c04200 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_bug863246_resource_uri.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +/* global SimpleTest SpecialPowers add_task */ + +function testResourceUri(aTest, aUri, aContentAccessible) { + return new Promise((aResolve) => { + let link = document.createElement("link"); + link.rel = "stylesheet"; + link.onload = () => { + SimpleTest.ok(aContentAccessible, aTest); + aResolve(); + }; + link.onerror = () => { + SimpleTest.ok(!aContentAccessible, aTest); + aResolve(); + }; + link.href = aUri; + document.head.appendChild(link); + }); +} + +add_task(async function() { + await testResourceUri( + "resource://content-accessible is content-accessible", + "resource://content-accessible/viewsource.css", + true); + await testResourceUri( + "resource://gre-resources is not content-accessible", + "resource://gre-resources/html.css", + false); + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.all_resource_uri_content_accessible", true], + ], + }); + await testResourceUri( + "security.all_resource_uri_content_accessible = true, resource://gre-resources is now content-accessible", + "resource://gre-resources/html.css", + true); +}); +</script> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_device_sensor_event.html b/browser/components/resistfingerprinting/test/mochitest/test_device_sensor_event.html new file mode 100644 index 0000000000..7ec4dce94f --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_device_sensor_event.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1369319 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1369319</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1369319 */ + SimpleTest.waitForExplicitFinish(); + window.onload = () => { + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": + [ + ["device.sensors.test.events", true], + ["privacy.resistFingerprinting", true], + ], + }, doTest); + }, window); + }; + + function doTest() { + window.addEventListener("devicemotion", () => { + ok(false, "The device motion event should not be fired."); + }, {once: true}); + + window.addEventListener("TestEvent", () => { + // If we receive this event without receiving a 'devicemotion' event, this means + // the device sensor event has been blocked correctly. + ok(true, "Got the 'TestEvent' event."); + SimpleTest.finish(); + }, {once: true}); + + window.dispatchEvent(new CustomEvent("TestEvent")); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html new file mode 100644 index 0000000000..95394ddb56 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1372069 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1372069</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + const BASE_GEO_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs"; + + /** Test for Bug 1372069 */ + /** Modified for Bug 1441295 */ + SimpleTest.waitForExplicitFinish(); + window.onload = () => { + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": + [ + ["privacy.resistFingerprinting", true], + ["geo.prompt.testing", true], + ["geo.prompt.testing.allow", true], + ["geo.provider.network.url", BASE_GEO_URL], + ], + }, doTest_getCurrentPosition); + }, window); + }; + + function doTest_getCurrentPosition() { + navigator.geolocation.getCurrentPosition( + (position) => { + ok(true, "Success callback is expected to be called"); + doTest_watchPosition(); + }, + (error) => { + ok(false, "Should be able to call success callback, Got error. code = " + error.code); + doTest_watchPosition(); + } + ); + } + + function doTest_watchPosition() { + let wid = navigator.geolocation.watchPosition( + (position) => { + ok(true, "Success callback is expected to be called"); + navigator.geolocation.clearWatch(wid); + SimpleTest.finish(); + }, + (error) => { + ok(false, "Should be able to call success callback, Got error. code = " + error.code); + navigator.geolocation.clearWatch(wid); + SimpleTest.finish(); + } + ); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info.html b/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info.html new file mode 100644 index 0000000000..08fdb0c852 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +/* global SimpleTest SpecialPowers */ + +SimpleTest.waitForExplicitFinish(); +document.addEventListener("DOMContentLoaded", function() { + SpecialPowers.pushPrefEnv({ + set: [ + ["dom.gamepad.test.enabled", true], + ["privacy.resistFingerprinting", true], + ], + }, function() { + // This test loads in an iframe, to ensure that the navigator instance is + // loaded with the correct value of the preference. + var iframe = document.createElement("iframe"); + iframe.allow = "gamepad"; + iframe.src = "test_hide_gamepad_info_iframe.html"; + document.body.appendChild(iframe); + }); +}); +</script> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info_iframe.html b/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info_iframe.html new file mode 100644 index 0000000000..5946e1ce6e --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_hide_gamepad_info_iframe.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="utf8"> +<!--<script src="/tests/SimpleTest/SimpleTest.js"></script>--> +<script> +var SimpleTest = window.parent.SimpleTest; + +function forceFail() { + SimpleTest.ok( + false, + "privacy.resistFingerprinting is true, should not receive any gamepad events" + ); +} + +window.addEventListener("gamepadconnected", forceFail); +window.addEventListener("gamepaddisconnected", forceFail); +window.addEventListener("gamepadbuttondown", forceFail); + +window.addEventListener("load", async () => { + const service = navigator.requestGamepadServiceTest(); + const buttonIndex = await service.addGamepad( + "test gamepad", // id + service.standardMapping, + service.noHand, + 4, // buttons + 2, + 0, + 0, + 0 + ); + + // Press a button to make the gamepad visible to the page. + await service.newButtonEvent(buttonIndex, 0, true, true); + + const { length } = navigator.getGamepads(); + SimpleTest.is( + length, + 0, + "privacy.resistFingerprinting is true, navigator.getGamepads() should always return an empty array" + ); + + // Attempt to force gamepad events to be fired, by simulating gamepad disconnect + await service.removeGamepad(buttonIndex); + SimpleTest.finish(); +}); +</script> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_iframe.html b/browser/components/resistfingerprinting/test/mochitest/test_iframe.html new file mode 100644 index 0000000000..f01809b28c --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_iframe.html @@ -0,0 +1,18 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<body> +<script> + add_task(async function() { + await SpecialPowers.pushPrefEnv({ + "set": [["privacy.resistFingerprinting", true]], + }); + is(screen.width, window.innerWidth, "Width should be spoofed"); + is(screen.height, window.innerHeight, "Height should be spoofed"); + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + is(iframe.contentWindow.screen.width, iframe.contentWindow.innerWidth, "Width should be spoofed in iframe"); + is(iframe.contentWindow.screen.height, iframe.contentWindow.innerHeight, "Height should be spoofed in iframe"); + }); +</script> +</body> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_keyboard_event.html b/browser/components/resistfingerprinting/test/mochitest/test_keyboard_event.html new file mode 100644 index 0000000000..72f2d29b90 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_keyboard_event.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1222285 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1222285</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1222285 */ + SimpleTest.waitForExplicitFinish(); + + window.onload = () => { + SpecialPowers.pushPrefEnv({"set": + [ + ["privacy.resistFingerprinting", true], + ], + }, doTestForSystemEventGroup); + }; + + // This test makes sure that system event group will still get real keyboard event. + function doTestForSystemEventGroup() { + SpecialPowers.addSystemEventListener(document, "keydown", + function eventHandler(aEvent) { + SpecialPowers.removeSystemEventListener(document, + "keydown", eventHandler, true); + + is(aEvent.code, "Minus", "The system group event should get real code."); + is(aEvent.keyCode, 63, "The system group event should get real keyCode."); + + doTestModifiersForSystemEventGroup(); + }, true); + + // Send key event to the system group. + synthesizeKey("\u00DF", {code: "Minus", keyCode: 63}); + } + + // Test that will system group event still get suppressed modifier keys + function doTestModifiersForSystemEventGroup() { + SpecialPowers.addSystemEventListener(document, "keydown", + function eventHandler(aEvent) { + SpecialPowers.removeSystemEventListener(document, + "keydown", eventHandler, true); + is(aEvent.key, "Alt", "The system group event get the suppressed keyboard event."); + + SimpleTest.finish(); + }, true); + + // Send key event to the system group. + synthesizeKey("KEY_Alt", {altKey: true}); + } + + </script> +</head> +<body> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_pointer_event.html b/browser/components/resistfingerprinting/test/mochitest/test_pointer_event.html new file mode 100644 index 0000000000..5b8271274e --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_pointer_event.html @@ -0,0 +1,242 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1363508 +--> +<head> + <meta charset="utf-8"> + <title>Test for Pointer Events spoofing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="target0" style="width: 50px; height: 50px; background: green"></div> +<div id="target1" style="width: 50px; height: 50px; background: black"></div> +<script type="application/javascript"> + + /** Test for Bug 1363508 */ + SimpleTest.waitForExplicitFinish(); + + var target0 = window.document.getElementById("target0"); + var target1 = window.document.getElementById("target1"); + var utils = SpecialPowers.Ci.nsIDOMWindowUtils; + + // A helper function to check that whether the pointer is spoofed correctly. + function checkPointerEvent(aEvent) { + is(aEvent.pointerId, utils.DEFAULT_MOUSE_POINTER_ID, + "The spoofed pointer event should always have the mouse pointer id."); + is(aEvent.width, 1, "The spoofed pointer event should always have width as 1."); + is(aEvent.height, 1, "The spoofed pointer event should always have width as 1."); + if (aEvent.buttons === 0) { + is(aEvent.pressure, 0.0, + "The spoofed pointer event should have pressure as 0.0 if it is not in a active buttons state."); + } else { + is(aEvent.pressure, 0.5, + "The spoofed pointer event should have pressure as 0.5 if it is in a active buttons state."); + } + is(aEvent.tangentialPressure, 0, "The spoofed pointer event should always have tangentialPressure as 0."); + is(aEvent.tiltX, 0, "The spoofed pointer event should always have tiltX as 0."); + is(aEvent.tiltY, 0, "The spoofed pointer event should always have tiltY as 0."); + is(aEvent.twist, 0, "The spoofed pointer event should always have twist as 0."); + is(aEvent.pointerType, "mouse", "The spoofed pointer event should always has mouse pointerType."); + is(aEvent.isPrimary, true, "The spoofed pointer event should only receive primary pointer events."); + } + + // A helper function to create a promise for waiting the event. + function promiseForEvent(aEventType, aCheckFunc) { + return new Promise(resolve => { + target0.addEventListener(aEventType, (event) => { + is(event.type, aEventType, "receive " + event.type + " on target0"); + aCheckFunc(event); + resolve(); + }, { once: true }); + }); + } + + // A test for pointer events from touch interface. + async function doTestForTouchPointerEvent() { + let eventPromises = [ + promiseForEvent("pointerover", checkPointerEvent), + promiseForEvent("pointerenter", checkPointerEvent), + promiseForEvent("pointerdown", checkPointerEvent), + promiseForEvent("pointermove", checkPointerEvent), + promiseForEvent("pointerup", checkPointerEvent), + promiseForEvent("pointerout", checkPointerEvent), + promiseForEvent("pointerleave", checkPointerEvent), + ]; + + synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 }); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 }); + synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_TOUCH, pressure: 0.75 }); + + await Promise.all(eventPromises); + } + + // A test for pointercancel event. + async function doTestForTouchPointerCancelEvent() { + let eventPromises = [ + promiseForEvent("pointerover", checkPointerEvent), + promiseForEvent("pointerenter", checkPointerEvent), + promiseForEvent("pointerdown", checkPointerEvent), + promiseForEvent("pointermove", checkPointerEvent), + promiseForEvent("pointercancel", checkPointerEvent), + promiseForEvent("pointerout", checkPointerEvent), + promiseForEvent("pointerleave", checkPointerEvent), + ]; + + synthesizeTouch(target0, 5, 5, { type: "touchstart" }); + synthesizeTouch(target0, 6, 6, { type: "touchmove" }); + synthesizeTouch(target0, 6, 6, { type: "touchcancel" }); + + await Promise.all(eventPromises); + } + + // A test for pointer events from pen interface. + async function doTestForPenPointerEvent() { + let eventPromises = [ + promiseForEvent("pointerover", checkPointerEvent), + promiseForEvent("pointerenter", checkPointerEvent), + promiseForEvent("pointerdown", checkPointerEvent), + promiseForEvent("pointermove", checkPointerEvent), + promiseForEvent("pointerup", checkPointerEvent), + promiseForEvent("pointerout", checkPointerEvent), + promiseForEvent("pointerleave", checkPointerEvent), + ]; + + synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: MouseEvent.MOZ_SOURCE_PEN }); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_PEN }); + synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_PEN }); + synthesizeMouse(target1, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_PEN }); + + await Promise.all(eventPromises); + } + + // A test for gotpointercapture and lostpointercapture events. + // We would also test releasePointerCapture for only accepting spoofed pointer + // Id here. + async function doTestForPointerCapture() { + // We test for both mouse and touch to see whether the capture events are + // filed properly. We don't check pen here since it won't file capture + // events. + let inputSources = [ MouseEvent.MOZ_SOURCE_MOUSE, + MouseEvent.MOZ_SOURCE_TOUCH ]; + + for (let inputSource of inputSources) { + function eventHandler(event) { + checkPointerEvent(event); + if (event.type === "pointerdown") { + target0.setPointerCapture(event.pointerId); + } else if (event.type === "pointermove") { + if (inputSource === MouseEvent.MOZ_SOURCE_TOUCH) { + try { + target0.releasePointerCapture(utils.DEFAULT_TOUCH_POINTER_ID); + ok(false, "The releasePointerCapture should fail here, but it is not."); + } catch (e) { + ok(true, "The releasePointerCapture fails properly."); + } + } + target0.releasePointerCapture(event.pointerId); + } + } + + let eventPromises = [ + promiseForEvent("pointerover", eventHandler), + promiseForEvent("pointerenter", eventHandler), + promiseForEvent("pointerdown", eventHandler), + promiseForEvent("gotpointercapture", eventHandler), + promiseForEvent("pointermove", eventHandler), + promiseForEvent("lostpointercapture", eventHandler), + promiseForEvent("pointerup", eventHandler), + promiseForEvent("pointerout", eventHandler), + promiseForEvent("pointerleave", eventHandler), + ]; + + synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource }); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource }); + synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource }); + synthesizeMouse(target1, 5, 5, { type: "mousemove", inputSource }); + + await Promise.all(eventPromises); + } + } + + // A test for setPointerCapture() for only accepting spoofed pointer id. + async function doTestForSetPointerCapture() { + function eventHandler(event) { + checkPointerEvent(event); + if (event.type === "pointerdown") { + try { + target0.setPointerCapture(utils.DEFAULT_TOUCH_POINTER_ID); + ok(false, "The setPointerCapture should fail here, but it is not."); + } catch (e) { + ok(true, "The setPointerCapture fails properly."); + } + } + } + + let eventPromises = [ + promiseForEvent("pointerover", eventHandler), + promiseForEvent("pointerenter", eventHandler), + promiseForEvent("pointerdown", eventHandler), + promiseForEvent("pointermove", eventHandler), + promiseForEvent("pointerup", eventHandler), + promiseForEvent("pointerout", eventHandler), + promiseForEvent("pointerleave", eventHandler), + ]; + + synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: MouseEvent.MOZ_SOURCE_TOUCH }); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: MouseEvent.MOZ_SOURCE_TOUCH }); + synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: MouseEvent.MOZ_SOURCE_TOUCH }); + + await Promise.all(eventPromises); + } + + // A test for assuring that script generated events won't be spoofed. + function doTestNoSpoofingForScriptGeneratedEvent() { + return new Promise(resolve => { + // Generate a custom pointer event by script. + let pointerEventCustom = new PointerEvent("pointerover", { + pointerId: utils.DEFAULT_TOUCH_POINTER_ID, + pointerType: "touch", + width: 5, + height: 5, + pressure: 0.75, + tangentialPressure: 0.5, + isPrimary: false, + }); + + target0.addEventListener("pointerover", (event) => { + // Check that script generated event is not spoofed. + is(event.pointerType, "touch", "The pointerEvent.pointerType is not spoofed."); + is(event.width, 5, "The pointerEvent.width is not spoofed."); + is(event.height, 5, "The pointerEvent.height is not spoofed."); + is(event.pressure, 0.75, "The pointerEvent.pressure is not spoofed."); + is(event.tangentialPressure, 0.5, "The pointerEvent.tangentialPressure is not spoofed."); + is(event.isPrimary, false, "The pointerEvent.isPrimary is not spoofed."); + resolve(); + }, { once: true }); + + target0.dispatchEvent(pointerEventCustom); + }); + } + + async function doTests() { + await doTestForTouchPointerEvent(); + await doTestForTouchPointerCancelEvent(); + await doTestForPenPointerEvent(); + await doTestForPointerCapture(); + await doTestForSetPointerCapture(); + await doTestNoSpoofingForScriptGeneratedEvent(); + + SimpleTest.finish(); + } + + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting", true]]}, + doTests); + }); + +</script> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/test_speech_synthesis.html b/browser/components/resistfingerprinting/test/mochitest/test_speech_synthesis.html new file mode 100644 index 0000000000..a1778a0a0d --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/test_speech_synthesis.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1333641 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1333641</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1333641 */ + SimpleTest.waitForExplicitFinish(); + window.onload = setupSpeechSynthesis; + + // This function setup the speechSynthesis and flip 'privacy.resistFingerprinting' + // after it has been setup correctly. + function setupSpeechSynthesis() { + window.speechSynthesis.addEventListener("voiceschanged", () => { + isnot(window.speechSynthesis.getVoices().length, 0, "Voices added"); + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": + [["privacy.resistFingerprinting", true]], + }, doGetVoicesTest); + }, window); + }, {once: true}); + + is(window.speechSynthesis.getVoices().length, 0, "No voices added initially"); + } + + function doGetVoicesTest() { + is(window.speechSynthesis.getVoices().length, 0, + "There should be no voices after fingerprinting resistance is enabled."); + doVoiceschangedEventTest(); + } + + function doVoiceschangedEventTest() { + window.speechSynthesis.addEventListener("voiceschanged", () => { + ok(false, "The voiceschanged event should not be fired."); + doSpeakTestAsync(); + }, {once: true}); + + window.addEventListener("TestEvent", () => { + // If we receive this event without receiving a 'voiceschanged' event, this means + // the voiceschanged event has been blocked correctly. + ok(true, "Got the 'TestEvent' event."); + doSpeakTestAsync(); + }, {once: true}); + + // Notify 'synth-voices-changed' for triggering the voiceschanged event. + SpecialPowers.Services.obs.notifyObservers(null, "synth-voices-changed"); + window.dispatchEvent(new CustomEvent("TestEvent")); + } + + // This tests Speak() and its asynchronousness. + function doSpeakTestAsync() { + // For non-e10s, this test will always fail since the event will be triggered immediately + // after speak() is called. So, new added events after speak() won't be called. We skip + // this test if it is non-e10s. + if (SpecialPowers.Services.appinfo.browserTabsRemoteAutostart) { + let utterance = new window.SpeechSynthesisUtterance("Hello, world!"); + window.speechSynthesis.speak(utterance); + + utterance.addEventListener("start", () => { + ok(false, "speechSynthesis should not start speaking if fingerprinting resistance is enabled."); + doSpeakTestSync(); + }, {once: true}); + + utterance.addEventListener("error", () => { + ok(true, "speechSynthesis.speak should fail if fingerprinting resistance is enabled."); + doSpeakTestSync(); + }, {once: true}); + } else { + doSpeakTestSync(); + } + } + + // This tests Speak() and its synchronousness. + function doSpeakTestSync() { + let utterance = new window.SpeechSynthesisUtterance("Hello, world!"); + utterance.addEventListener("start", () => { + ok(false, "speechSynthesis should not start speaking if fingerprinting resistance is enabled."); + SimpleTest.finish(); + }, {once: true}); + + utterance.addEventListener("error", () => { + ok(true, "speechSynthesis.speak should fail if fingerprinting resistance is enabled."); + SimpleTest.finish(); + }, {once: true}); + + window.speechSynthesis.speak(utterance); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/browser/components/resistfingerprinting/test/mochitest/worker_child.js b/browser/components/resistfingerprinting/test/mochitest/worker_child.js new file mode 100644 index 0000000000..fa340fc652 --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/worker_child.js @@ -0,0 +1,28 @@ +let timeStampCodes; +let worker = new Worker("worker_grandchild.js"); + +function listenToParent(event) { + self.removeEventListener("message", listenToParent); + timeStampCodes = event.data; + + let timeStamps = []; + for (let timeStampCode of timeStampCodes) { + timeStamps.push(eval(timeStampCode)); + } + // Send the timeStamps to the parent. + postMessage(timeStamps); + + // Tell the grandchild to start. + worker.postMessage(timeStampCodes); +} + +// The worker grandchild will send results back. +function listenToChild(event) { + worker.removeEventListener("message", listenToChild); + // Pass the results to the parent. + postMessage(event.data); + worker.terminate(); +} + +worker.addEventListener("message", listenToChild); +self.addEventListener("message", listenToParent); diff --git a/browser/components/resistfingerprinting/test/mochitest/worker_grandchild.js b/browser/components/resistfingerprinting/test/mochitest/worker_grandchild.js new file mode 100644 index 0000000000..ef9940d7ab --- /dev/null +++ b/browser/components/resistfingerprinting/test/mochitest/worker_grandchild.js @@ -0,0 +1,10 @@ +self.addEventListener("message", function (event) { + let timeStampCodes = event.data; + + let timeStamps = []; + for (let timeStampCode of timeStampCodes) { + timeStamps.push(eval(timeStampCode)); + } + // Send the timeStamps to the parent. + postMessage(timeStamps); +}); |