diff options
Diffstat (limited to 'browser/components/originattributes/test')
86 files changed, 4931 insertions, 0 deletions
diff --git a/browser/components/originattributes/test/browser/browser.ini b/browser/components/originattributes/test/browser/browser.ini new file mode 100644 index 0000000000..a01f901744 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser.ini @@ -0,0 +1,96 @@ +[DEFAULT] +tags = usercontextid firstpartyisolation originattributes +support-files = + dummy.html + file_broadcastChannel.html + file_broadcastChanneliFrame.html + file_cache.html + file_favicon.html + file_favicon.png + file_favicon.png^headers^ + file_favicon_cache.html + file_favicon_cache.png + file_favicon_thirdParty.html + file_firstPartyBasic.html + file_postMessage.html + file_postMessageSender.html + file_saveAs.sjs + file_sharedworker.html + file_sharedworker.js + file_thirdPartyChild.audio.ogg + file_thirdPartyChild.embed.png + file_thirdPartyChild.fetch.html + file_thirdPartyChild.font.woff + file_thirdPartyChild.iframe.html + file_thirdPartyChild.favicon.png + file_thirdPartyChild.img.png + file_thirdPartyChild.import.js + file_thirdPartyChild.link.css + file_thirdPartyChild.object.png + file_thirdPartyChild.request.html + file_thirdPartyChild.script.js + file_thirdPartyChild.sharedworker.js + file_thirdPartyChild.track.vtt + file_thirdPartyChild.video.ogv + file_thirdPartyChild.worker.fetch.html + file_thirdPartyChild.worker.js + file_thirdPartyChild.worker.request.html + file_thirdPartyChild.worker.xhr.html + file_thirdPartyChild.xhr.html + file_windowOpenerRestriction.html + file_windowOpenerRestrictionTarget.html + head.js + test.js + test.js^headers^ + test.html + test2.html + test2.js + test2.js^headers^ + test_firstParty.html + test_firstParty_cookie.html + test_firstParty_html_redirect.html + test_firstParty_http_redirect.html + test_firstParty_http_redirect.html^headers^ + test_firstParty_http_redirect_to_same_domain.html + test_firstParty_http_redirect_to_same_domain.html^headers^ + test_firstParty_iframe_http_redirect.html + test_firstParty_postMessage.html + test_form.html + window.html + window2.html + window3.html + window_redirect.html + worker_blobify.js + !/toolkit/content/tests/browser/common/mockTransfer.js +# We don't want to run tests using BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN +# (5 - aka Dynamic First Party Isolation) yet. +prefs = + network.cookie.cookieBehavior=4 + +[browser_broadcastChannel.js] +[browser_cache.js] +skip-if = verify +[browser_cookieIsolation.js] +[browser_favicon_firstParty.js] +[browser_favicon_userContextId.js] +[browser_firstPartyIsolation.js] +skip-if = debug #Bug 1345346 +[browser_firstPartyIsolation_about_newtab.js] +[browser_firstPartyIsolation_aboutPages.js] +[browser_firstPartyIsolation_blobURI.js] +[browser_firstPartyIsolation_js_uri.js] +[browser_firstPartyIsolation_saveAs.js] +[browser_localStorageIsolation.js] +[browser_blobURLIsolation.js] +skip-if = (verify && debug && (os == 'win')) +[browser_imageCacheIsolation.js] +[browser_sharedworker.js] +[browser_httpauth.js] +[browser_clientAuth.js] +skip-if = verify +[browser_cacheAPI.js] +[browser_permissions.js] +[browser_postMessage.js] +[browser_sanitize.js] +skip-if = (os == 'win') || (os == "linux" && bits == 64) #Bug 1544810 +[browser_windowOpenerRestriction.js] diff --git a/browser/components/originattributes/test/browser/browser_blobURLIsolation.js b/browser/components/originattributes/test/browser/browser_blobURLIsolation.js new file mode 100644 index 0000000000..7b42f09883 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_blobURLIsolation.js @@ -0,0 +1,121 @@ +/** + * Bug 1264573 - A test case for blob url isolation. + */ + +const TEST_PAGE = + "http://mochi.test:8888/browser/browser/components/" + + "originattributes/test/browser/file_firstPartyBasic.html"; +const SCRIPT_WORKER_BLOBIFY = "worker_blobify.js"; + +function page_blobify(browser, input) { + return SpecialPowers.spawn(browser, [input], function(contentInput) { + return { + blobURL: content.URL.createObjectURL(new content.Blob([contentInput])), + }; + }); +} + +function page_deblobify(browser, blobURL) { + return SpecialPowers.spawn(browser, [blobURL], async function( + contentBlobURL + ) { + if ("error" in contentBlobURL) { + return contentBlobURL; + } + contentBlobURL = contentBlobURL.blobURL; + + function blobURLtoBlob(aBlobURL) { + return new content.Promise(function(resolve) { + let xhr = new content.XMLHttpRequest(); + xhr.open("GET", aBlobURL, true); + xhr.onload = function() { + resolve(xhr.response); + }; + xhr.onerror = function() { + resolve("xhr error"); + }; + xhr.responseType = "blob"; + xhr.send(); + }); + } + + function blobToString(blob) { + return new content.Promise(function(resolve) { + let fileReader = new content.FileReader(); + fileReader.onload = function() { + resolve(fileReader.result); + }; + fileReader.readAsText(blob); + }); + } + + let blob = await blobURLtoBlob(contentBlobURL); + if (blob == "xhr error") { + return "xhr error"; + } + + return blobToString(blob); + }); +} + +function workerIO(browser, what, message) { + return SpecialPowers.spawn( + browser, + [ + { + scriptFile: SCRIPT_WORKER_BLOBIFY, + message: { message, what }, + }, + ], + function(args) { + if (!content.worker) { + content.worker = new content.Worker(args.scriptFile); + } + let promise = new content.Promise(function(resolve) { + let listenFunction = function(event) { + content.worker.removeEventListener("message", listenFunction); + resolve(event.data); + }; + content.worker.addEventListener("message", listenFunction); + }); + content.worker.postMessage(args.message); + return promise; + } + ); +} + +let worker_blobify = (browser, input) => workerIO(browser, "blobify", input); +let worker_deblobify = (browser, blobURL) => + workerIO(browser, "deblobify", blobURL); + +function doTest(blobify, deblobify) { + let blobURL = null; + return async function(browser) { + if (blobURL === null) { + let input = Math.random().toString(); + blobURL = await blobify(browser, input); + return input; + } + let result = await deblobify(browser, blobURL); + blobURL = null; + return result; + }; +} + +let tests = []; +for (let blobify of [page_blobify, worker_blobify]) { + for (let deblobify of [page_deblobify, worker_deblobify]) { + tests.push(doTest(blobify, deblobify)); + } +} + +async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.bloburl_per_agent_cluster", false], + ["dom.security.https_first", false], + ], + }); +} + +IsolationTestTools.runTests(TEST_PAGE, tests, null, setup); diff --git a/browser/components/originattributes/test/browser/browser_broadcastChannel.js b/browser/components/originattributes/test/browser/browser_broadcastChannel.js new file mode 100644 index 0000000000..ae81b1a4ce --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_broadcastChannel.js @@ -0,0 +1,55 @@ +/* + * Bug 1264571 - A test case of broadcast channels for first party isolation. + */ + +const TEST_DOMAIN = "http://example.net/"; +const TEST_PATH = + TEST_DOMAIN + "browser/browser/components/originattributes/test/browser/"; +const TEST_PAGE = TEST_PATH + "file_broadcastChannel.html"; + +// IsolationTestTools flushes all preferences +// hence we explicitly pref off https-first mode +async function prefOffHttpsFirstMode() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); +} + +async function doTest(aBrowser) { + let response = await SpecialPowers.spawn(aBrowser, [], async function() { + let displayItem = content.document.getElementById("display"); + + // If there is nothing in the 'display', we will try to send a message to + // the broadcast channel and wait until this message has been delivered. + // The way that how we make sure the message is delivered is based on an + // iframe which will reply everything it receives from the broadcast channel + // to the current window through the postMessage. So, we can know that the + // boradcast message is sent successfully when the window receives a message + // from the iframe. + if (displayItem.innerHTML === "") { + let data = Math.random().toString(); + + let receivedData = await new Promise(resolve => { + let listenFunc = event => { + content.removeEventListener("message", listenFunc); + resolve(event.data); + }; + + let bc = new content.BroadcastChannel("testBroadcastChannel"); + + content.addEventListener("message", listenFunc); + bc.postMessage(data); + }); + + Assert.equal(receivedData, data, "The value should be the same."); + + return receivedData; + } + + return displayItem.innerHTML; + }); + + return response; +} + +IsolationTestTools.runTests(TEST_PAGE, doTest, null, prefOffHttpsFirstMode); diff --git a/browser/components/originattributes/test/browser/browser_cache.js b/browser/components/originattributes/test/browser/browser_cache.js new file mode 100644 index 0000000000..631caa78ca --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_cache.js @@ -0,0 +1,350 @@ +/* + * Bug 1264577 - A test case for testing caches of various submodules. + * This test case will load two pages that each page loads various resources + * within the same third party domain for the same originAttributes or different + * originAttributes. And then, it verifies the number of cache entries and + * the originAttributes of loading channels. If these two pages belong to + * the same originAttributes, the number of cache entries for a certain + * resource would be one. Otherwise, it would be two. + */ + +const CC = Components.Constructor; + +let protocolProxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" +].getService(Ci.nsIProtocolProxyService); + +const TEST_DOMAIN = "http://example.net"; +const TEST_PATH = "/browser/browser/components/originattributes/test/browser/"; +const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "file_cache.html"; + +let suffixes = [ + "iframe.html", + "link.css", + "script.js", + "img.png", + "object.png", + "embed.png", + "xhr.html", + "worker.xhr.html", + "audio.ogg", + "video.ogv", + "track.vtt", + "fetch.html", + "worker.fetch.html", + "request.html", + "worker.request.html", + "import.js", + "worker.js", + "sharedworker.js", + "font.woff", +]; + +// A random value for isolating video/audio elements across different tests. +let randomSuffix; + +function clearAllImageCaches() { + let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + let imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content +} + +function cacheDataForContext(loadContextInfo) { + return new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); +} + +let countMatchingCacheEntries = function(cacheEntries, domain, fileSuffix) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes("file_thirdPartyChild." + fileSuffix)).length; +}; + +function observeChannels(onChannel) { + // We use a dummy proxy filter to catch all channels, even those that do not + // generate an "http-on-modify-request" notification, such as link preconnects. + let proxyFilter = { + applyFilter(aChannel, aProxy, aCallback) { + // We have the channel; provide it to the callback. + onChannel(aChannel); + // Pass on aProxy unmodified. + aCallback.onProxyFilterResult(aProxy); + }, + }; + protocolProxyService.registerChannelFilter(proxyFilter, 0); + // Return the stop() function: + return () => protocolProxyService.unregisterChannelFilter(proxyFilter); +} + +function startObservingChannels(aMode) { + let stopObservingChannels = observeChannels(function(channel) { + let originalURISpec = channel.originalURI.spec; + if (originalURISpec.includes("example.net")) { + let loadInfo = channel.loadInfo; + + switch (aMode) { + case TEST_MODE_FIRSTPARTY: + ok( + loadInfo.originAttributes.firstPartyDomain === "example.com" || + loadInfo.originAttributes.firstPartyDomain === "example.org", + "first party for " + + originalURISpec + + " is " + + loadInfo.originAttributes.firstPartyDomain + ); + break; + + case TEST_MODE_NO_ISOLATION: + ok( + ChromeUtils.isOriginAttributesEqual( + loadInfo.originAttributes, + ChromeUtils.fillNonDefaultOriginAttributes() + ), + "OriginAttributes for " + originalURISpec + " is default." + ); + break; + + case TEST_MODE_CONTAINERS: + ok( + loadInfo.originAttributes.userContextId === 1 || + loadInfo.originAttributes.userContextId === 2, + "userContextId for " + + originalURISpec + + " is " + + loadInfo.originAttributes.userContextId + ); + break; + + default: + ok(false, "Unknown test mode."); + } + } + }); + return stopObservingChannels; +} + +let stopObservingChannels; + +// The init function, which clears image and network caches, and generates +// the random value for isolating video and audio elements across different +// test runs. +async function doInit(aMode) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.predictor.enabled", false], + ["network.predictor.enable-prefetch", false], + ["privacy.partition.network_state", false], + ["dom.security.https_first", false], + ], + }); + clearAllImageCaches(); + + Services.cache2.clear(); + + randomSuffix = Math.random(); + stopObservingChannels = startObservingChannels(aMode); +} + +// In the test function, we dynamically generate the video and audio element, +// and assign a random suffix to their URL to isolate them across different +// test runs. +async function doTest(aBrowser) { + let argObj = { + randomSuffix, + urlPrefix: TEST_DOMAIN + TEST_PATH, + }; + + await SpecialPowers.spawn(aBrowser, [argObj], async function(arg) { + content.windowUtils.clearSharedStyleSheetCache(); + + let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv"; + let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg"; + let trackURL = arg.urlPrefix + "file_thirdPartyChild.track.vtt"; + let URLSuffix = "?r=" + arg.randomSuffix; + + // Create the audio and video elements. + let audio = content.document.createElement("audio"); + let video = content.document.createElement("video"); + let audioSource = content.document.createElement("source"); + let audioTrack = content.document.createElement("track"); + + // Append the audio and track element into the body, and wait until they're finished. + await new content.Promise(resolve => { + let audioLoaded = false; + let trackLoaded = false; + + let audioListener = () => { + Assert.ok(true, `Audio suspended: ${audioURL + URLSuffix}`); + audio.removeEventListener("suspend", audioListener); + + audioLoaded = true; + if (audioLoaded && trackLoaded) { + resolve(); + } + }; + + let trackListener = () => { + Assert.ok(true, `Audio track loaded: ${audioURL + URLSuffix}`); + audioTrack.removeEventListener("load", trackListener); + + trackLoaded = true; + if (audioLoaded && trackLoaded) { + resolve(); + } + }; + + Assert.ok(true, `Loading audio: ${audioURL + URLSuffix}`); + + // Add the event listeners before everything in case we lose events. + audioTrack.addEventListener("load", trackListener); + audio.addEventListener("suspend", audioListener); + + // Assign attributes for the audio element. + audioSource.setAttribute("src", audioURL + URLSuffix); + audioSource.setAttribute("type", "audio/ogg"); + audioTrack.setAttribute("src", trackURL); + audioTrack.setAttribute("kind", "subtitles"); + audioTrack.setAttribute("default", true); + + audio.appendChild(audioSource); + audio.appendChild(audioTrack); + audio.autoplay = true; + + content.document.body.appendChild(audio); + }); + + // Append the video element into the body, and wait until it's finished. + await new content.Promise(resolve => { + let listener = () => { + Assert.ok(true, `Video suspended: ${videoURL + URLSuffix}`); + video.removeEventListener("suspend", listener); + resolve(); + }; + + Assert.ok(true, `Loading video: ${videoURL + URLSuffix}`); + + // Add the event listener before everything in case we lose the event. + video.addEventListener("suspend", listener); + + // Assign attributes for the video element. + video.setAttribute("src", videoURL + URLSuffix); + video.setAttribute("type", "video/ogg"); + + content.document.body.appendChild(video); + }); + }); + + return 0; +} + +// The check function, which checks the number of cache entries. +async function doCheck(aShouldIsolate, aInputA, aInputB) { + let expectedEntryCount = 1; + let data = []; + data = data.concat( + await cacheDataForContext(Services.loadContextInfo.default) + ); + data = data.concat( + await cacheDataForContext(Services.loadContextInfo.private) + ); + data = data.concat( + await cacheDataForContext(Services.loadContextInfo.custom(true, {})) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(false, { userContextId: 1 }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(true, { userContextId: 1 }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(false, { userContextId: 2 }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(true, { userContextId: 2 }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(false, { + firstPartyDomain: "example.com", + }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(true, { firstPartyDomain: "example.com" }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(false, { + firstPartyDomain: "example.org", + }) + ) + ); + data = data.concat( + await cacheDataForContext( + Services.loadContextInfo.custom(true, { firstPartyDomain: "example.org" }) + ) + ); + + if (aShouldIsolate) { + expectedEntryCount = 2; + } + + for (let suffix of suffixes) { + let foundEntryCount = countMatchingCacheEntries( + data, + "example.net", + suffix + ); + let result = expectedEntryCount === foundEntryCount; + ok( + result, + "Cache entries expected for " + + suffix + + ": " + + expectedEntryCount + + ", and found " + + foundEntryCount + ); + } + + stopObservingChannels(); + stopObservingChannels = undefined; + return true; +} + +let testArgs = { + url: TEST_PAGE, + firstFrameSetting: DEFAULT_FRAME_SETTING, + secondFrameSetting: [TEST_TYPE_FRAME], +}; + +IsolationTestTools.runTests(testArgs, doTest, doCheck, doInit); diff --git a/browser/components/originattributes/test/browser/browser_cacheAPI.js b/browser/components/originattributes/test/browser/browser_cacheAPI.js new file mode 100644 index 0000000000..55d879da34 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_cacheAPI.js @@ -0,0 +1,24 @@ +const requestURL = "https://test1.example.com"; + +function getResult(aBrowser) { + return SpecialPowers.spawn(aBrowser, [requestURL], async function(url) { + let cache = await content.caches.open("TEST_CACHE"); + let response = await cache.match(url); + if (response) { + return response.statusText; + } + let result = Math.random().toString(); + response = new content.Response("", { statusText: result }); + await cache.put(url, response); + return result; + }); +} + +IsolationTestTools.runTests( + "https://test2.example.com", + getResult, + null, + null, + false, + /* aUseHttps */ true +); diff --git a/browser/components/originattributes/test/browser/browser_clientAuth.js b/browser/components/originattributes/test/browser/browser_clientAuth.js new file mode 100644 index 0000000000..f93ec1dff2 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_clientAuth.js @@ -0,0 +1,49 @@ +let certCached = true; +let secondTabStarted = false; + +function onCertDialogLoaded(subject) { + certCached = false; + // Click OK. + subject.acceptDialog(); +} + +Services.obs.addObserver(onCertDialogLoaded, "cert-dialog-loaded"); + +registerCleanupFunction(() => { + Services.obs.removeObserver(onCertDialogLoaded, "cert-dialog-loaded"); +}); + +async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.default_personal_cert", "Ask Every Time"], + ["privacy.partition.network_state", false], + ], + }); +} + +function getResult() { + // The first tab always returns true. + if (!secondTabStarted) { + certCached = true; + secondTabStarted = true; + return true; + } + + // The second tab returns true if the cert is cached, so it will be different + // from the result of the first tab, and considered isolated. + let ret = certCached; + certCached = true; + secondTabStarted = false; + return ret; +} + +// aGetResultImmediately must be true because we need to get the result before +// the next tab is opened. +IsolationTestTools.runTests( + "https://requireclientcert.example.com", + getResult, + null, // aCompareResultFunc + setup, // aBeginFunc + true +); // aGetResultImmediately diff --git a/browser/components/originattributes/test/browser/browser_cookieIsolation.js b/browser/components/originattributes/test/browser/browser_cookieIsolation.js new file mode 100644 index 0000000000..6411e23924 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_cookieIsolation.js @@ -0,0 +1,42 @@ +/** + * Bug 1312541 - A test case for document.cookie isolation. + */ + +const TEST_PAGE = + "https://example.net/browser/browser/components/" + + "originattributes/test/browser/file_firstPartyBasic.html"; + +// Use a random key so we don't access it in later tests. +const key = "key" + Math.random().toString(); +const re = new RegExp(key + "=([0-9.]+)"); + +// Define the testing function +function doTest(aBrowser) { + return SpecialPowers.spawn(aBrowser, [key, re], function( + contentKey, + contentRe + ) { + let result = contentRe.exec(content.document.cookie); + if (result) { + return result[1]; + } + // No value is found, so we create one. + let value = Math.random().toString(); + content.document.cookie = + contentKey + "=" + value + "; SameSite=None; Secure;"; + return value; + }); +} + +registerCleanupFunction(() => { + Services.cookies.removeAll(); +}); + +IsolationTestTools.runTests( + TEST_PAGE, + doTest, + null, + null, + false, + true /* aUseHttps */ +); diff --git a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js new file mode 100644 index 0000000000..f077a4c705 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js @@ -0,0 +1,514 @@ +/** + * Bug 1277803 - A test case for testing favicon loading across different first party domains. + */ + +if (SpecialPowers.useRemoteSubframes) { + requestLongerTimeout(2); +} + +const CC = Components.Constructor; + +const { PlacesUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesUtils.sys.mjs" +); +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +let EventUtils = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +const FIRST_PARTY_ONE = "example.com"; +const FIRST_PARTY_TWO = "example.org"; +const THIRD_PARTY = "example.net"; + +const TEST_SITE_ONE = "https://" + FIRST_PARTY_ONE; +const TEST_SITE_TWO = "https://" + FIRST_PARTY_TWO; +const THIRD_PARTY_SITE = "https://" + THIRD_PARTY; +const TEST_DIRECTORY = + "/browser/browser/components/originattributes/test/browser/"; + +const TEST_PAGE = TEST_DIRECTORY + "file_favicon.html"; +const TEST_THIRD_PARTY_PAGE = TEST_DIRECTORY + "file_favicon_thirdParty.html"; +const TEST_CACHE_PAGE = TEST_DIRECTORY + "file_favicon_cache.html"; + +const FAVICON_URI = TEST_DIRECTORY + "file_favicon.png"; +const TEST_FAVICON_CACHE_URI = TEST_DIRECTORY + "file_favicon_cache.png"; + +const ICON_DATA = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABH0lEQVRYw2P8////f4YBBEwMAwxGHcBCUMX/91DGOSj/BpT/DkpzQChGBSjfBErLQsVZhmoI/L8LpRdD6X1QietQGhYy7FB5aAgwmkLpBKi4BZTPMThDgBGjHIDF+f9mKD0fKvGBRKNdoF7sgPL1saaJwZgGDkJ9vpZMn8PAHqg5G9FyifBgD4H/W9HyOWrU/f+DIzHhkoeZxxgzZEIAVtJ9RxX+Q6DAxCmP3byhXxkxshAs5odqbcioAY3UC1CBLyTGOTqAmsfAOWRCwBvqxV0oIUB2OQAzDy3/D+a6wB7q8mCU2vD/nw94GziYIQOtDRn9oXz+IZMGBKGMbCjNh9Ii+v8HR4uIAUeLiEEbb9twELaIRlqrmHG0bzjiHQAA1LVfww8jwM4AAAAASUVORK5CYII="; + +let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function clearAllImageCaches() { + let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + let imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content +} + +function clearAllPlacesFavicons() { + let faviconService = Cc["@mozilla.org/browser/favicon-service;1"].getService( + Ci.nsIFaviconService + ); + + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + if (aTopic === "places-favicons-expired") { + resolve(); + Services.obs.removeObserver(observer, "places-favicons-expired"); + } + }, + }; + + Services.obs.addObserver(observer, "places-favicons-expired"); + faviconService.expireAllFavicons(); + }); +} + +function observeFavicon(aFirstPartyDomain, aExpectedCookie, aPageURI) { + let expectedPrincipal = Services.scriptSecurityManager.createContentPrincipal( + aPageURI, + { firstPartyDomain: aFirstPartyDomain } + ); + + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + // Make sure that the topic is 'http-on-modify-request'. + if (aTopic === "http-on-modify-request") { + // We check the firstPartyDomain for the originAttributes of the loading + // channel. All requests for the favicon should contain the correct + // firstPartyDomain. There are two requests for a favicon loading, one + // from the Places library and one from the XUL image. The difference + // of them is the loading principal. The Places will use the content + // principal and the XUL image will use the system principal. + + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + let loadingPrincipal = reqLoadInfo.loadingPrincipal; + let triggeringPrincipal = reqLoadInfo.triggeringPrincipal; + + // Make sure this is a favicon request. + if (!httpChannel.URI.spec.endsWith(FAVICON_URI)) { + return; + } + + // Check the first party domain. + is( + reqLoadInfo.originAttributes.firstPartyDomain, + aFirstPartyDomain, + "The loadInfo has correct first party domain" + ); + + ok( + loadingPrincipal.equals(expectedPrincipal), + "The loadingPrincipal of favicon loads should be the content prinicpal" + ); + ok( + triggeringPrincipal.equals(expectedPrincipal), + "The triggeringPrincipal of favicon loads should be the content prinicpal" + ); + + let faviconCookie = httpChannel.getRequestHeader("cookie"); + + is( + faviconCookie, + aExpectedCookie, + "The cookie of the favicon loading is correct." + ); + } else { + ok(false, "Received unexpected topic: ", aTopic); + } + + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + }, + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +function waitOnFaviconResponse(aFaviconURL) { + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + if ( + aTopic === "http-on-examine-response" || + aTopic === "http-on-examine-cached-response" + ) { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let loadInfo = httpChannel.loadInfo; + + if (httpChannel.URI.spec !== aFaviconURL) { + return; + } + + let result = { + topic: aTopic, + firstPartyDomain: loadInfo.originAttributes.firstPartyDomain, + }; + + resolve(result); + Services.obs.removeObserver(observer, "http-on-examine-response"); + Services.obs.removeObserver( + observer, + "http-on-examine-cached-response" + ); + } + }, + }; + + Services.obs.addObserver(observer, "http-on-examine-response"); + Services.obs.addObserver(observer, "http-on-examine-cached-response"); + }); +} + +function waitOnFaviconLoaded(aFaviconURL) { + return PlacesTestUtils.waitForNotification( + "favicon-changed", + events => events.some(e => e.faviconUrl == aFaviconURL), + "places" + ); +} + +async function openTab(aURL) { + let tab = BrowserTestUtils.addTab(gBrowser, aURL); + + // Select tab and make sure its browser is focused. + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return { tab, browser }; +} + +async function assignCookiesUnderFirstParty(aURL, aFirstParty, aCookieValue) { + // Open a tab under the given aFirstParty, and this tab will have an + // iframe which loads the aURL. + let tabInfo = await openTabInFirstParty(aURL, aFirstParty); + + // Add cookies into the iframe. + await SpecialPowers.spawn(tabInfo.browser, [aCookieValue], async function( + value + ) { + content.document.cookie = value + "; SameSite=None; Secure;"; + }); + + BrowserTestUtils.removeTab(tabInfo.tab); +} + +async function generateCookies(aThirdParty) { + // we generate two different cookies for two first party domains. + let cookies = []; + cookies.push(Math.random().toString()); + cookies.push(Math.random().toString()); + + let firstSiteURL; + let secondSiteURL; + + if (aThirdParty) { + // Add cookies into the third party site with different first party domain. + firstSiteURL = THIRD_PARTY_SITE; + secondSiteURL = THIRD_PARTY_SITE; + } else { + // Add cookies into sites. + firstSiteURL = TEST_SITE_ONE; + secondSiteURL = TEST_SITE_TWO; + } + + await assignCookiesUnderFirstParty(firstSiteURL, TEST_SITE_ONE, cookies[0]); + await assignCookiesUnderFirstParty(secondSiteURL, TEST_SITE_TWO, cookies[1]); + + return cookies; +} + +function assertIconIsData(item) { + let icon = item.getAttribute("image"); + is( + icon.substring(0, 5), + "data:", + "Expected the image element to be a data URI" + ); + is(icon, ICON_DATA, "Expected to see the correct data."); +} + +async function doTest(aTestPage, aExpectedCookies, aFaviconURL) { + let firstPageURI = Services.io.newURI(TEST_SITE_ONE + aTestPage); + let secondPageURI = Services.io.newURI(TEST_SITE_TWO + aTestPage); + + // Start to observe the event of that favicon has been fully loaded. + let promiseFaviconLoaded = waitOnFaviconLoaded(aFaviconURL); + + // Start to observe the favicon requests earlier in case we miss it. + let promiseObserveFavicon = observeFavicon( + FIRST_PARTY_ONE, + aExpectedCookies[0], + firstPageURI + ); + + // Open the tab for the first site. + let tabInfo = await openTab(TEST_SITE_ONE + aTestPage); + + // Waiting until favicon requests are all made. + await promiseObserveFavicon; + + // Waiting until favicon loaded. + await promiseFaviconLoaded; + + assertIconIsData(tabInfo.tab); + + BrowserTestUtils.removeTab(tabInfo.tab); + // FIXME: We need to wait for the next event tick here to avoid observing + // the previous tab info in the next step (bug 1446725). + await new Promise(executeSoon); + + // Start to observe the favicon requests earlier in case we miss it. + promiseObserveFavicon = observeFavicon( + FIRST_PARTY_TWO, + aExpectedCookies[1], + secondPageURI + ); + + // Open the tab for the second site. + tabInfo = await openTab(TEST_SITE_TWO + aTestPage); + + // Waiting until favicon requests are all made. + await promiseObserveFavicon; + + BrowserTestUtils.removeTab(tabInfo.tab); +} + +async function doTestForAllTabsFavicon( + aTestPage, + aExpectedCookies, + aIsThirdParty +) { + let faviconURI = aIsThirdParty + ? THIRD_PARTY_SITE + FAVICON_URI + : TEST_SITE_ONE + FAVICON_URI; + + // Set the 'overflow' attribute to make allTabs button available. + let tabBrowser = document.getElementById("tabbrowser-tabs"); + tabBrowser.setAttribute("overflow", true); + + // Start to observe the event of that the favicon has been fully loaded. + let promiseFaviconLoaded = waitOnFaviconLoaded(faviconURI); + + // Open the tab for the first site. + let tabInfo = await openTab(TEST_SITE_ONE + aTestPage); + + // Waiting until the favicon loaded. + await promiseFaviconLoaded; + + assertIconIsData(tabInfo.tab); + + gTabsPanel.init(); + + // Make the popup of allTabs showing up and trigger the loading of the favicon. + let allTabsView = document.getElementById("allTabsMenu-allTabsView"); + let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + gTabsPanel.showAllTabsPanel(); + await allTabsPopupShownPromise; + + assertIconIsData( + gTabsPanel.allTabsViewTabs.lastElementChild.firstElementChild + ); + + // Close the popup of allTabs and wait until it's done. + let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent( + allTabsView.panelMultiView, + "PanelMultiViewHidden" + ); + gTabsPanel.hideAllTabsPanel(); + await allTabsPopupHiddenPromise; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + + faviconURI = aIsThirdParty + ? THIRD_PARTY_SITE + FAVICON_URI + : TEST_SITE_TWO + FAVICON_URI; + + // Start to observe the event of that favicon has been fully loaded. + promiseFaviconLoaded = waitOnFaviconLoaded(faviconURI); + + // Open the tab for the second site. + tabInfo = await openTab(TEST_SITE_TWO + aTestPage); + + // Wait until the favicon is fully loaded. + await promiseFaviconLoaded; + + assertIconIsData(tabInfo.tab); + + // Make the popup of allTabs showing up again. + allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + gTabsPanel.showAllTabsPanel(); + await allTabsPopupShownPromise; + + assertIconIsData( + gTabsPanel.allTabsViewTabs.lastElementChild.firstElementChild + ); + + // Close the popup of allTabs and wait until it's done. + allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent( + allTabsView.panelMultiView, + "PanelMultiViewHidden" + ); + gTabsPanel.hideAllTabsPanel(); + await allTabsPopupHiddenPromise; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + + // Reset the 'overflow' attribute to make the allTabs button hidden again. + tabBrowser.removeAttribute("overflow"); +} + +add_setup(async function() { + // Make sure first party isolation is enabled. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate", true]], + }); +}); + +// A clean up function to prevent affecting other tests. +registerCleanupFunction(() => { + // Clear all cookies. + Services.cookies.removeAll(); + + // Clear all image caches and network caches. + clearAllImageCaches(); + + Services.cache2.clear(); +}); + +add_task(async function test_favicon_firstParty() { + for (let testThirdParty of [false, true]) { + // Clear all image caches and network caches before running the test. + clearAllImageCaches(); + + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + let cookies = await generateCookies(testThirdParty); + + if (testThirdParty) { + await doTest( + TEST_THIRD_PARTY_PAGE, + cookies, + THIRD_PARTY_SITE + FAVICON_URI + ); + } else { + await doTest(TEST_PAGE, cookies, TEST_SITE_ONE + FAVICON_URI); + } + } +}); + +add_task(async function test_allTabs_favicon_firstParty() { + for (let testThirdParty of [false, true]) { + // Clear all image caches and network caches before running the test. + clearAllImageCaches(); + + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + let cookies = await generateCookies(testThirdParty); + + if (testThirdParty) { + await doTestForAllTabsFavicon( + TEST_THIRD_PARTY_PAGE, + cookies, + testThirdParty + ); + } else { + await doTestForAllTabsFavicon(TEST_PAGE, cookies, testThirdParty); + } + } +}); + +add_task(async function test_favicon_cache_firstParty() { + // Clear all image caches and network caches before running the test. + clearAllImageCaches(); + + Services.cache2.clear(); + + // Start to observer the event of that favicon has been fully loaded and cached. + let promiseForFaviconLoaded = waitOnFaviconLoaded( + THIRD_PARTY_SITE + TEST_FAVICON_CACHE_URI + ); + + // Start to observer for the favicon response of the first tab. + let responsePromise = waitOnFaviconResponse( + THIRD_PARTY_SITE + TEST_FAVICON_CACHE_URI + ); + + // Open the tab for the first site. + let tabInfoA = await openTab(TEST_SITE_ONE + TEST_CACHE_PAGE); + + // Waiting for the favicon response. + let response = await responsePromise; + + // Make sure the favicon is loaded through the network and its first party domain is correct. + is( + response.topic, + "http-on-examine-response", + "The favicon image should be loaded through network." + ); + is( + response.firstPartyDomain, + FIRST_PARTY_ONE, + "We should only observe the network response for the first first party." + ); + + // Waiting until the favicon has been loaded and cached. + await promiseForFaviconLoaded; + + // Here, we are going to observe the favicon response for the third tab which + // opens with the second first party. + let promiseForFaviconResponse = waitOnFaviconResponse( + THIRD_PARTY_SITE + TEST_FAVICON_CACHE_URI + ); + + // Open the tab for the second site. + let tabInfoB = await openTab(TEST_SITE_TWO + TEST_CACHE_PAGE); + + // Wait for the favicon response. In this case, we suppose to catch the + // response for the third tab but not the second tab since it will not + // go through the network. + response = await promiseForFaviconResponse; + + // Check that the favicon response has came from the network and it has the + // correct first party domain. + is( + response.topic, + "http-on-examine-response", + "The favicon image should be loaded through network again." + ); + is( + response.firstPartyDomain, + FIRST_PARTY_TWO, + "We should only observe the network response for the second first party." + ); + + BrowserTestUtils.removeTab(tabInfoA.tab); + BrowserTestUtils.removeTab(tabInfoB.tab); +}); diff --git a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js new file mode 100644 index 0000000000..8386223451 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js @@ -0,0 +1,404 @@ +/** + * Bug 1277803 - A test caes for testing favicon loading across different userContextId. + */ + +if (SpecialPowers.useRemoteSubframes) { + requestLongerTimeout(2); +} + +let EventUtils = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +const TEST_SITE = "https://example.org"; +const TEST_THIRD_PARTY_SITE = "https://example.net"; + +const TEST_PAGE = + TEST_SITE + + "/browser/browser/components/originattributes/" + + "test/browser/file_favicon.html"; +const FAVICON_URI = + TEST_SITE + + "/browser/browser/components/originattributes/" + + "test/browser/file_favicon.png"; +const TEST_THIRD_PARTY_PAGE = + "http://example.net/browser/browser/components/" + + "originattributes/test/browser/file_favicon_thirdParty.html"; +const THIRD_PARTY_FAVICON_URI = + TEST_THIRD_PARTY_SITE + + "/browser/browser/components/" + + "originattributes/test/browser/file_favicon.png"; + +const USER_CONTEXT_ID_PERSONAL = 1; +const USER_CONTEXT_ID_WORK = 2; + +let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function clearAllImageCaches() { + var tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + var imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content +} + +function clearAllPlacesFavicons() { + let faviconService = Cc["@mozilla.org/browser/favicon-service;1"].getService( + Ci.nsIFaviconService + ); + + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + if (aTopic === "places-favicons-expired") { + resolve(); + Services.obs.removeObserver(observer, "places-favicons-expired"); + } + }, + }; + + Services.obs.addObserver(observer, "places-favicons-expired"); + faviconService.expireAllFavicons(); + }); +} + +function FaviconObserver( + aUserContextId, + aExpectedCookie, + aPageURI, + aFaviconURL +) { + this.reset(aUserContextId, aExpectedCookie, aPageURI, aFaviconURL); +} + +FaviconObserver.prototype = { + observe(aSubject, aTopic, aData) { + // Make sure that the topic is 'http-on-modify-request'. + if (aTopic === "http-on-modify-request") { + // We check the userContextId for the originAttributes of the loading + // channel. All requests for the favicon should contain the correct + // userContextId. There are two requests for a favicon loading, one + // from the Places library and one from the XUL image. The difference + // of them is the loading principal. The Places will use the content + // principal and the XUL image will use the system principal. + + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + let loadingPrincipal, triggeringPrincipal; + + // Make sure this is a favicon request. + if (httpChannel.URI.spec !== this._faviconURL) { + return; + } + + if (reqLoadInfo) { + loadingPrincipal = reqLoadInfo.loadingPrincipal; + triggeringPrincipal = reqLoadInfo.triggeringPrincipal; + } + + // Check the userContextId. + is( + reqLoadInfo.originAttributes.userContextId, + this._curUserContextId, + "The loadInfo has correct userContextId" + ); + + ok( + loadingPrincipal.equals(this._expectedPrincipal), + "The loadingPrincipal of favicon loading from content should be the content prinicpal" + ); + ok( + triggeringPrincipal.equals(this._expectedPrincipal), + "The triggeringPrincipal of favicon loading from content should be the content prinicpal" + ); + + let faviconCookie = httpChannel.getRequestHeader("cookie"); + + is( + faviconCookie, + this._expectedCookie, + "The cookie of the favicon loading is correct." + ); + } else { + ok(false, "Received unexpected topic: ", aTopic); + } + + this._faviconLoaded.resolve(); + }, + + reset(aUserContextId, aExpectedCookie, aPageURI, aFaviconURL) { + this._curUserContextId = aUserContextId; + this._expectedCookie = aExpectedCookie; + this._expectedPrincipal = Services.scriptSecurityManager.createContentPrincipal( + aPageURI, + { userContextId: aUserContextId } + ); + this._faviconURL = aFaviconURL; + this._faviconLoaded = PromiseUtils.defer(); + }, + + get promise() { + return this._faviconLoaded.promise; + }, +}; + +function waitOnFaviconLoaded(aFaviconURL) { + return PlacesTestUtils.waitForNotification( + "favicon-changed", + events => events.some(e => e.faviconUrl == aFaviconURL), + "places" + ); +} + +async function generateCookies(aHost) { + // we generate two different cookies for two userContextIds. + let cookies = []; + cookies.push(Math.random().toString()); + cookies.push(Math.random().toString()); + + // Then, we add cookies into the site for 'personal' and 'work'. + let tabInfoA = await openTabInUserContext(aHost, USER_CONTEXT_ID_PERSONAL); + let tabInfoB = await openTabInUserContext(aHost, USER_CONTEXT_ID_WORK); + + await SpecialPowers.spawn(tabInfoA.browser, [cookies[0]], async function( + value + ) { + content.document.cookie = value; + }); + + await SpecialPowers.spawn(tabInfoB.browser, [cookies[1]], async function( + value + ) { + content.document.cookie = value; + }); + + BrowserTestUtils.removeTab(tabInfoA.tab); + BrowserTestUtils.removeTab(tabInfoB.tab); + + return cookies; +} + +async function doTest(aTestPage, aFaviconHost, aFaviconURL) { + let cookies = await generateCookies(aFaviconHost); + let pageURI = makeURI(aTestPage); + + // Create the observer object for observing request channels of the personal + // container. + let observer = new FaviconObserver( + USER_CONTEXT_ID_PERSONAL, + cookies[0], + pageURI, + aFaviconURL + ); + + // Add the observer earlier in case we miss it. + let promiseWaitOnFaviconLoaded = waitOnFaviconLoaded(aFaviconURL); + + Services.obs.addObserver(observer, "http-on-modify-request"); + + // Open the tab with the personal container. + let tabInfo = await openTabInUserContext(aTestPage, USER_CONTEXT_ID_PERSONAL); + + // Waiting for favicon requests are all made. + await observer.promise; + // Waiting for favicon loaded. + await promiseWaitOnFaviconLoaded; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + // FIXME: We need to wait for the next event tick here to avoid observing + // the previous tab info in the next step (bug 1446725). + await new Promise(executeSoon); + + // Reset the observer for observing requests for the work container. + observer.reset(USER_CONTEXT_ID_WORK, cookies[1], pageURI, aFaviconURL); + tabInfo = await openTabInUserContext(aTestPage, USER_CONTEXT_ID_WORK); + + // Waiting for favicon requests are all made. + await observer.promise; + + Services.obs.removeObserver(observer, "http-on-modify-request"); + + BrowserTestUtils.removeTab(tabInfo.tab); +} + +function assertIconIsData(item) { + is( + item.getAttribute("image").substring(0, 5), + "data:", + "Expected the image element to be a data URI" + ); +} + +async function doTestForAllTabsFavicon(aTestPage, aFaviconHost, aFaviconURL) { + await generateCookies(aFaviconHost); + + // Set the 'overflow' attribute to make allTabs button available. + let tabBrowser = document.getElementById("tabbrowser-tabs"); + tabBrowser.setAttribute("overflow", true); + + // Add the observer earlier in case we miss it. + let promiseWaitOnFaviconLoaded = waitOnFaviconLoaded(aFaviconURL); + + // Open the tab with the personal container. + let tabInfo = await openTabInUserContext(aTestPage, USER_CONTEXT_ID_PERSONAL); + + // Waiting for favicon loaded. + await promiseWaitOnFaviconLoaded; + + // We need to clear the image cache here for making sure the network request will + // be made for the favicon of allTabs menuitem. + clearAllImageCaches(); + + gTabsPanel.init(); + + // Make the popup of allTabs showing up and trigger the loading of the favicon. + let allTabsView = document.getElementById("allTabsMenu-allTabsView"); + let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + gTabsPanel.showAllTabsPanel(); + await allTabsPopupShownPromise; + + assertIconIsData( + gTabsPanel.allTabsViewTabs.lastElementChild.firstElementChild + ); + + // Close the popup of allTabs and wait until it's done. + let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent( + allTabsView.panelMultiView, + "PanelMultiViewHidden" + ); + gTabsPanel.hideAllTabsPanel(); + await allTabsPopupHiddenPromise; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + // FIXME: We need to wait for the next event tick here to avoid observing + // the previous tab info in the next step (bug 1446725). + await new Promise(executeSoon); + + // Open the tab under the work container and wait until the favicon is loaded. + promiseWaitOnFaviconLoaded = waitOnFaviconLoaded(aFaviconURL); + tabInfo = await openTabInUserContext(aTestPage, USER_CONTEXT_ID_WORK); + await promiseWaitOnFaviconLoaded; + + // Clear the image cache again. + clearAllImageCaches(); + + // Make the popup of allTabs showing up again. + allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + gTabsPanel.showAllTabsPanel(); + await allTabsPopupShownPromise; + + assertIconIsData( + gTabsPanel.allTabsViewTabs.lastElementChild.firstElementChild + ); + + // Close the popup of allTabs and wait until it's done. + allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent( + allTabsView.panelMultiView, + "PanelMultiViewHidden" + ); + gTabsPanel.hideAllTabsPanel(); + await allTabsPopupHiddenPromise; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + + // Reset the 'overflow' attribute to make the allTabs button hidden again. + tabBrowser.removeAttribute("overflow"); +} + +add_setup(async function() { + // Make sure userContext is enabled. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.enabled", true]], + }); +}); + +// A clean up function to prevent affecting other tests. +registerCleanupFunction(() => { + // Clear all cookies. + Services.cookies.removeAll(); + + // Clear all image caches and network caches. + clearAllImageCaches(); + + Services.cache2.clear(); + + // Clear Places favicon caches. + clearAllPlacesFavicons(); +}); + +add_task(async function test_favicon_userContextId() { + // Clear all image caches before running the test. + clearAllImageCaches(); + + // Clear all network caches. + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + await doTest(TEST_PAGE, TEST_SITE, FAVICON_URI); +}); + +add_task(async function test_thirdPartyFavicon_userContextId() { + // Clear all image caches before running the test. + clearAllImageCaches(); + + // Clear all network caches. + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + await doTest( + TEST_THIRD_PARTY_PAGE, + TEST_THIRD_PARTY_SITE, + THIRD_PARTY_FAVICON_URI + ); +}); + +add_task(async function test_allTabs_favicon_userContextId() { + // Clear all image caches before running the test. + clearAllImageCaches(); + + // Clear all network caches. + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + await doTestForAllTabsFavicon(TEST_PAGE, TEST_SITE, FAVICON_URI); +}); + +add_task(async function test_allTabs_thirdPartyFavicon_userContextId() { + // Clear all image caches before running the test. + clearAllImageCaches(); + + // Clear all network caches. + Services.cache2.clear(); + + // Clear Places favicon caches. + await clearAllPlacesFavicons(); + + await doTestForAllTabsFavicon( + TEST_THIRD_PARTY_PAGE, + TEST_THIRD_PARTY_SITE, + THIRD_PARTY_FAVICON_URI + ); +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation.js new file mode 100644 index 0000000000..189f37b14d --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation.js @@ -0,0 +1,495 @@ +const BASE_URL = + "https://example.net/browser/browser/components/originattributes/test/browser/"; +const EXAMPLE_BASE_URL = BASE_URL.replace("example.net", "example.com"); +const BASE_DOMAIN = "example.net"; + +add_setup(async function() { + Services.prefs.setBoolPref("privacy.firstparty.isolate", true); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("privacy.firstparty.isolate"); + Services.cookies.removeAll(); + Services.cache2.clear(); + }); +}); + +/** + * Test for the top-level document and child iframes should have the + * firstPartyDomain attribute. + */ +add_task(async function principal_test() { + let tab = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty.html" + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true, function(url) { + return url == BASE_URL + "test_firstParty.html"; + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.ok( + true, + "document principal: " + content.document.nodePrincipal.origin + ); + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + "", + "top-level docShell shouldn't have firstPartyDomain attribute." + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + + for (let i = 1; i < 4; i++) { + let iframe = content.document.getElementById("iframe" + i); + await SpecialPowers.spawn(iframe, [attrs.firstPartyDomain], function( + firstPartyDomain + ) { + Assert.ok( + true, + "iframe principal: " + content.document.nodePrincipal.origin + ); + + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + firstPartyDomain, + "iframe's docshell should have firstPartyDomain" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + firstPartyDomain, + "iframe should have firstPartyDomain" + ); + }); + } + } + ); + + gBrowser.removeTab(tab); +}); + +/** + * Test for the cookie jars of the top-level document and child iframe should be + * isolated by firstPartyDomain. + */ +add_task(async function cookie_test() { + let tab = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty_cookie.html" + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true); + + let count = 0; + for (let cookie of Services.cookies.cookies) { + count++; + Assert.equal(cookie.value, "foo", "Cookie value should be foo"); + Assert.equal( + cookie.originAttributes.firstPartyDomain, + BASE_DOMAIN, + "Cookie's origin attributes should be " + BASE_DOMAIN + ); + } + + // one cookie is from requesting test.js from top-level doc, and the other from + // requesting test2.js from iframe test2.html. + Assert.equal(count, 2, "Should have two cookies"); + + gBrowser.removeTab(tab); +}); + +/** + * Test for after redirect, the top-level document should update the firstPartyDomain + * attribute. However if the redirect is happening on the iframe, the attribute + * should remain the same. + */ +add_task(async function redirect_test() { + let tab = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty_http_redirect.html" + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ firstPartyDomain: "example.com" }], + async function(attrs) { + Assert.ok( + true, + "document principal: " + content.document.nodePrincipal.origin + ); + Assert.ok(true, "document uri: " + content.document.documentURI); + + Assert.equal( + content.document.documentURI, + "https://example.com/browser/browser/components/originattributes/test/browser/dummy.html", + "The page should have been redirected to https://example.com/browser/browser/components/originattributes/test/browser/dummy.html" + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + } + ); + + // Since this is a HTML redirect, we wait until the final page is loaded. + let tab2 = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty_html_redirect.html" + ); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, function( + url + ) { + return url == "https://example.com/"; + }); + + await SpecialPowers.spawn( + tab2.linkedBrowser, + [{ firstPartyDomain: "example.com" }], + async function(attrs) { + Assert.ok( + true, + "2nd tab document principal: " + content.document.nodePrincipal.origin + ); + Assert.ok(true, "2nd tab document uri: " + content.document.documentURI); + Assert.equal( + content.document.documentURI, + "https://example.com/", + "The page should have been redirected to https://example.com" + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + } + ); + + let tab3 = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty_iframe_http_redirect.html" + ); + await BrowserTestUtils.browserLoaded(tab3.linkedBrowser, true, function(url) { + return url == BASE_URL + "test_firstParty_iframe_http_redirect.html"; + }); + + // This redirect happens on the iframe, so unlike the two redirect tests above, + // the firstPartyDomain should still stick to the current top-level document, + // which is example.net. + await SpecialPowers.spawn( + tab3.linkedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + let iframe = content.document.getElementById("iframe1"); + SpecialPowers.spawn(iframe, [attrs.firstPartyDomain], function( + firstPartyDomain + ) { + Assert.ok( + true, + "iframe document principal: " + content.document.nodePrincipal.origin + ); + Assert.ok(true, "iframe document uri: " + content.document.documentURI); + + Assert.equal( + content.document.documentURI, + "https://example.com/browser/browser/components/originattributes/test/browser/dummy.html", + "The page should have been redirected to https://example.com/browser/browser/components/originattributes/test/browser/dummy.html" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + firstPartyDomain, + "The iframe should have firstPartyDomain: " + firstPartyDomain + ); + }); + } + ); + + gBrowser.removeTab(tab); + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab3); +}); + +/** + * Test for postMessage between document and iframe. + */ +add_task(async function postMessage_test() { + let tab = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "test_firstParty_postMessage.html" + ); + + // The top-level page will post a message to its child iframe, and wait for + // another message from the iframe, once it receives the message, it will + // create another iframe, dummy.html. + // So we wait until dummy.html is loaded + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true, function(url) { + return url == BASE_URL + "dummy.html"; + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + Assert.ok( + true, + "document principal: " + content.document.nodePrincipal.origin + ); + let value = content.document.getElementById("message").textContent; + Assert.equal(value, "OK"); + }); + + gBrowser.removeTab(tab); +}); + +/** + * When the web page calls window.open, the new window should have the same + * firstPartyDomain attribute. + */ +add_task(async function openWindow_test() { + Services.prefs.setIntPref("browser.link.open_newwindow", 2); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.link.open_newwindow"); + }); + + let tab = BrowserTestUtils.addTab(gBrowser, BASE_URL + "window.html"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + let promise = new Promise(resolve => { + content.addEventListener("message", resolve, { once: true }); + }); + let w = Cu.unwaiveXrays(content.wrappedJSObject.open()); + w.document.body.innerHTML = `<iframe id='iframe1' onload="window.opener.postMessage('ready', '*');" src='data:text/plain,test2'></iframe>`; + + await promise; + + Assert.equal( + w.docShell.getOriginAttributes().firstPartyDomain, + attrs.firstPartyDomain, + "window.open() should have correct firstPartyDomain attribute" + ); + Assert.equal( + w.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have correct firstPartyDomain" + ); + + let iframe = w.document.getElementById("iframe1"); + await SpecialPowers.spawn(iframe, [attrs.firstPartyDomain], function( + firstPartyDomain + ) { + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + firstPartyDomain, + "iframe's docshell should have correct rirstPartyDomain" + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + firstPartyDomain, + "iframe should have correct firstPartyDomain" + ); + }); + + w.close(); + } + ); + + gBrowser.removeTab(tab); +}); + +/** + * When the web page calls window.open, the top-level docshell in the new + * created window will have firstPartyDomain set. + */ +add_task(async function window_open_redirect_test() { + Services.prefs.setIntPref("browser.link.open_newwindow", 2); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.link.open_newwindow"); + }); + + let tab = BrowserTestUtils.addTab( + gBrowser, + BASE_URL + "window_redirect.html" + ); + let win = await BrowserTestUtils.waitForNewWindow({ + url: BASE_URL + "dummy.html", + }); + + await SpecialPowers.spawn( + win.gBrowser.selectedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + attrs.firstPartyDomain, + "window.open() should have firstPartyDomain attribute" + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + } + ); + + gBrowser.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * When the web page calls window.open, the top-level docshell in the new + * created window will inherit the firstPartyDomain attribute. + * However the top-level document will override the firstPartyDomain if the + * document is from another domain. + */ +add_task(async function window_open_iframe_test() { + Services.prefs.setIntPref("browser.link.open_newwindow", 2); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.link.open_newwindow"); + }); + + let tab = BrowserTestUtils.addTab(gBrowser, BASE_URL + "window2.html"); + let url = EXAMPLE_BASE_URL + "test_firstParty.html"; + info("Waiting for window with url " + url); + let win = await BrowserTestUtils.waitForNewWindow({ url }); + + await SpecialPowers.spawn( + win.gBrowser.selectedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + attrs.firstPartyDomain, + "window.open() should have firstPartyDomain attribute" + ); + + // The document is https://example.com/browser/browser/components/originattributes/test/browser/test_firstParty.html + // so the firstPartyDomain will be overriden to 'example.com'. + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + "example.com", + "The document should have firstPartyDomain" + ); + + let iframe = content.document.getElementById("iframe1"); + SpecialPowers.spawn(iframe, [], function() { + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + "example.com", + "iframe's docshell should have firstPartyDomain" + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + "example.com", + "iframe should have firstPartyDomain" + ); + }); + } + ); + + gBrowser.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Test for the loadInfo->TriggeringPrincipal is the document itself. + */ +add_task(async function form_test() { + let tab = BrowserTestUtils.addTab(gBrowser, BASE_URL + "test_form.html"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + + let submit = content.document.getElementById("submit"); + submit.click(); + } + ); + + gBrowser.removeTab(tab); +}); + +/** + * Another test for loadInfo->TriggeringPrincipal in the window.open case. + */ +add_task(async function window_open_form_test() { + Services.prefs.setIntPref("browser.link.open_newwindow", 2); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("browser.link.open_newwindow"); + }); + + let tab = BrowserTestUtils.addTab(gBrowser, BASE_URL + "window3.html"); + let url = EXAMPLE_BASE_URL + "test_form.html"; + info("Waiting for window with url " + url); + let win = await BrowserTestUtils.waitForNewWindow({ url }); + + await SpecialPowers.spawn( + win.gBrowser.selectedBrowser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.equal( + content.docShell.getOriginAttributes().firstPartyDomain, + attrs.firstPartyDomain, + "window.open() should have firstPartyDomain attribute" + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + "example.com", + "The document should have firstPartyDomain" + ); + + let submit = content.document.getElementById("submit"); + submit.click(); + } + ); + + gBrowser.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * A test for using an IP address as the first party domain. + */ +add_task(async function ip_address_test() { + const ipAddr = "127.0.0.1"; + const ipHost = `http://${ipAddr}/browser/browser/components/originattributes/test/browser/`; + + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + }); + + let tab = BrowserTestUtils.addTab(gBrowser, ipHost + "test_firstParty.html"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ firstPartyDomain: ipAddr }], + async function(attrs) { + Assert.ok( + true, + "document principal: " + content.document.nodePrincipal.origin + ); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The firstPartyDomain should be set properly for the IP address" + ); + } + ); + + gBrowser.removeTab(tab); +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js new file mode 100644 index 0000000000..7dd236e81e --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_aboutPages.js @@ -0,0 +1,258 @@ +if (SpecialPowers.useRemoteSubframes) { + requestLongerTimeout(2); +} + +add_setup(async function() { + Services.prefs.setBoolPref("privacy.firstparty.isolate", true); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref("privacy.firstparty.isolate"); + }); +}); + +/** + * For loading the initial about:blank in e10s mode, it will be loaded with + * NullPrincipal, and we also test if the firstPartyDomain of the origin + * attributes is got from the origin itself. + */ +add_task(async function test_remote_window_open_aboutBlank() { + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + + Assert.ok(browser.isRemoteBrowser, "should be a remote browser"); + + await SpecialPowers.spawn(browser, [], async function() { + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of remote about:blank should be a NullPrincipal." + ); + + let str = content.document.nodePrincipal.originNoSuffix; + let expectDomain = + str.substring("moz-nullprincipal:{".length, str.length - 1) + ".mozilla"; + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + expectDomain, + "remote about:blank should have firstPartyDomain set to " + expectDomain + ); + }); + + win.close(); +}); + +/** + * For loading the initial about:blank in non-e10s mode, it will be loaded with + * a null principal. So we test if it has correct firstPartyDomain set. + */ +add_task(async function test_nonremote_window_open_aboutBlank() { + if (SpecialPowers.useRemoteSubframes) { + ok(true, "We cannot test non-e10s mode in Fission. So, we skip this."); + return; + } + + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: false }); + let browser = win.gBrowser.selectedBrowser; + + Assert.ok(!browser.isRemoteBrowser, "shouldn't be a remote browser"); + + await SpecialPowers.spawn(browser, [], async function() { + info("origin " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of remote about:blank should be a NullPrincipal." + ); + + let str = content.document.nodePrincipal.originNoSuffix; + let expectDomain = + str.substring("moz-nullprincipal:{".length, str.length - 1) + ".mozilla"; + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + expectDomain, + "non-remote about:blank should have firstPartyDomain set to " + + expectDomain + ); + }); + + win.close(); +}); + +/** + * Check if data: URI inherits firstPartyDomain from about:blank correctly. + */ +add_task(async function test_remote_window_open_data_uri() { + // allow top level data: URI navigations, otherwise + // <a href="data:" would fail. + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", false]], + }); + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + + await SpecialPowers.spawn(browser, [], () => { + content.document.body.innerHTML = ` + <a href="data:text/plain,hello" id="test">hello</a>`; + + let element = content.document.getElementById("test"); + element.click(); + }); + + await BrowserTestUtils.browserLoaded(browser, false, function(url) { + return url == "data:text/plain,hello"; + }); + + await SpecialPowers.spawn(browser, [], async function() { + Assert.ok(true, "origin: " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of data: document should be a NullPrincipal." + ); + + Assert.ok( + content.document.nodePrincipal.originAttributes.firstPartyDomain != "", + "data: URI should have firstPartyDomain set." + ); + }); + + win.close(); +}); + +/** + * data: document contains an iframe, and we test that iframe should inherit + * origin attributes from the data: document. + */ +add_task(async function test_remote_window_open_data_uri2() { + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + const TEST_PAGE = + "https://example.net/browser/browser/components/originattributes/test/browser/test2.html"; + + // The iframe test2.html will fetch test2.js, which will have cookies. + const DATA_URI = `data:text/html, + <iframe id="iframe1" src="${TEST_PAGE}"></iframe>`; + BrowserTestUtils.loadURI(browser, DATA_URI); + await BrowserTestUtils.browserLoaded(browser, true, TEST_PAGE); + + await SpecialPowers.spawn(browser, [], async function() { + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of data: document should be a NullPrincipal." + ); + + Assert.ok( + content.document.nodePrincipal.originAttributes.firstPartyDomain != "", + "data: URI should have firstPartyDomain set." + ); + + let iframe = content.document.getElementById("iframe1"); + await SpecialPowers.spawn( + iframe, + [content.document.nodePrincipal.originAttributes.firstPartyDomain], + function(firstPartyDomain) { + Assert.ok( + true, + "iframe principal: " + content.document.nodePrincipal.origin + ); + + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + firstPartyDomain, + "iframe should inherit firstPartyDomain from parent document." + ); + Assert.equal( + content.document.cookie, + "test2=foo", + "iframe should have cookies" + ); + } + ); + }); + + win.close(); +}); + +/** + * about: pages should have firstPartyDomain set when we enable first party isolation. + */ +add_task(async function test_aboutURL() { + let aboutURLs = []; + + // List of about: URLs that will initiate network requests. + let networkURLs = ["credits", "logins"]; + + for (let cid in Cc) { + let result = cid.match( + /@mozilla.org\/network\/protocol\/about;1\?what\=(.*)$/ + ); + if (!result) { + continue; + } + + let aboutType = result[1]; + let contract = "@mozilla.org/network/protocol/about;1?what=" + aboutType; + try { + let am = Cc[contract].getService(Ci.nsIAboutModule); + let uri = Services.io.newURI("about:" + aboutType); + let flags = am.getURIFlags(uri); + + // We load pages with URI_SAFE_FOR_UNTRUSTED_CONTENT set, this means they + // are not loaded with System Principal but with content principal. + // Also we skip pages with HIDE_FROM_ABOUTABOUT, some of them may have + // errors while loading. + if ( + flags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT && + !(flags & Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT) && + !networkURLs.includes(aboutType) && + // handle about:newtab in browser_firstPartyIsolation_about_newtab.js + aboutType !== "newtab" && + // protections kicks of async messaging as soon as it loads, + // this test closes the tab too soon causing errors + aboutType !== "protections" + ) { + aboutURLs.push(aboutType); + } + } catch (e) { + // getService might have thrown if the component doesn't actually + // implement nsIAboutModule + } + } + + for (let url of aboutURLs) { + let tab = BrowserTestUtils.addTab(gBrowser, "about:" + url); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let attrs = { + firstPartyDomain: "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }; + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ attrs, url }], + async function(args) { + Assert.ok( + true, + "loading page about:" + + args.url + + ", origin is " + + content.document.nodePrincipal.origin + ); + Assert.ok(true, "principal " + content.document.nodePrincipal); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + args.attrs.firstPartyDomain, + "The about page should have firstPartyDomain set" + ); + Assert.ok( + content.document.nodePrincipal.isContentPrincipal, + "The principal should be a content principal." + ); + } + ); + + gBrowser.removeTab(tab); + } +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_about_newtab.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_about_newtab.js new file mode 100644 index 0000000000..ccb52fa96f --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_about_newtab.js @@ -0,0 +1,53 @@ +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.newtab.preload", false], + ["privacy.firstparty.isolate", true], + ], + }); +}); + +/** + * Test about:newtab will have firstPartytDomain set when we enable the pref. + * + * We split about:newtab from browser_firstPartyIsolation_aboutPages.js because + * about:newtab needs special care. + * + * In the original test browser_firstPartyIsolation_aboutPages.js, when calling + * tabbrowser.addTab, if it found out the uri is about:newtab, it will use + * the preloaded browser, however the preloaded browser is loaded before when we + * turn on the firstPartyIsolation pref, which won't have the pref set. + * + * To prevent to use the preloaded browser, a simple trick is open a window + * first. + **/ +add_task(async function test_aboutNewTab() { + // In Fission, we cannot open a non-remote window. + let win = await BrowserTestUtils.openNewBrowserWindow({ + remote: SpecialPowers.useRemoteSubframes, + }); + let gbrowser = win.gBrowser; + let tab = BrowserTestUtils.addTab(gbrowser, "about:newtab"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let attrs = { + firstPartyDomain: "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }; + await SpecialPowers.spawn(tab.linkedBrowser, [{ attrs }], async function( + args + ) { + Assert.ok(true, "principal " + content.document.nodePrincipal.origin); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + args.attrs.firstPartyDomain, + "about:newtab should have firstPartyDomain set" + ); + Assert.ok( + content.document.nodePrincipal.isContentPrincipal, + "The principal should be a content principal." + ); + }); + + gbrowser.removeTab(tab); + win.close(); +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_blobURI.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_blobURI.js new file mode 100644 index 0000000000..4e19f8fe6a --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_blobURI.js @@ -0,0 +1,114 @@ +add_setup(async function() { + Services.prefs.setBoolPref("privacy.firstparty.isolate", true); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref("privacy.firstparty.isolate"); + Services.cookies.removeAll(); + }); +}); + +/** + * First we generate a Blob URI by using URL.createObjectURL(new Blob(..)); + * then we navigate to this Blob URI, hence to make the top-level document URI + * is Blob URI. + * Later we create an iframe on this Blob: document, and we test that the iframe + * has correct firstPartyDomain. + */ +add_task(async function test_blob_uri_inherit_oa_from_content() { + const BASE_URI = + "http://mochi.test:8888/browser/browser/components/" + + "originattributes/test/browser/dummy.html"; + const BASE_DOMAIN = "mochi.test"; + + // First we load a normal web page. + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + BrowserTestUtils.loadURI(browser, BASE_URI); + await BrowserTestUtils.browserLoaded(browser); + + // Then navigate to the blob: URI. + await SpecialPowers.spawn( + browser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + + // Now we use createObjectURL to generate a blob URI and navigate to it. + let url = content.window.URL.createObjectURL( + new content.window.Blob( + [ + `<script src="http://mochi.test:8888/browser/browser/components/originattributes/test/browser/test.js"></script>`, + ], + { type: "text/html" } + ) + ); + content.document.location = url; + } + ); + + // Wait for the Blob: URI to be loaded. + await BrowserTestUtils.browserLoaded(browser, false, function(url) { + info("BrowserTestUtils.browserLoaded url=" + url); + return url.startsWith("blob:http://mochi.test:8888/"); + }); + + // We verify the blob document has correct origin attributes. + // Then we inject an iframe to it. + await SpecialPowers.spawn( + browser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + Assert.ok( + content.document.documentURI.startsWith("blob:http://mochi.test:8888/"), + "the document URI should be a blob URI." + ); + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + attrs.firstPartyDomain, + "The document should have firstPartyDomain" + ); + + let iframe = content.document.createElement("iframe"); + iframe.src = "http://example.com"; + iframe.id = "iframe1"; + content.document.body.appendChild(iframe); + + // Wait for the iframe to be loaded. + await new content.Promise(done => { + iframe.addEventListener( + "load", + function() { + done(); + }, + { capture: true, once: true } + ); + }); + } + ); + + // Finally we verify the iframe has correct origin attributes. + await SpecialPowers.spawn( + browser, + [{ firstPartyDomain: BASE_DOMAIN }], + async function(attrs) { + let iframe = content.document.getElementById("iframe1"); + await SpecialPowers.spawn(iframe, [attrs.firstPartyDomain], function( + firstPartyDomain + ) { + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + firstPartyDomain, + "iframe should inherit firstPartyDomain from blob: URI" + ); + }); + } + ); + + win.close(); +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_js_uri.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_js_uri.js new file mode 100644 index 0000000000..2b44e21c6d --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_js_uri.js @@ -0,0 +1,90 @@ +add_setup(async function() { + Services.prefs.setBoolPref("privacy.firstparty.isolate", true); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref("privacy.firstparty.isolate"); + }); +}); + +add_task(async function test_remote_window_open_js_uri() { + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + + Assert.ok(browser.isRemoteBrowser, "should be a remote browser"); + + BrowserTestUtils.loadURI(browser, `javascript:1;`); + + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async function() { + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of remote javascript: should be a NullPrincipal." + ); + + let str = content.document.nodePrincipal.originNoSuffix; + let expectDomain = + str.substring("moz-nullprincipal:{".length, str.length - 1) + ".mozilla"; + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + expectDomain, + "remote javascript: should have firstPartyDomain set to " + expectDomain + ); + }); + + win.close(); +}); + +add_task(async function test_remote_window_open_js_uri2() { + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + + Assert.ok(browser.isRemoteBrowser, "should be a remote browser"); + + BrowserTestUtils.loadURI( + browser, + `javascript: + let iframe = document.createElement("iframe"); + iframe.src = "http://example.com"; + iframe.id = "iframe1"; + document.body.appendChild(iframe); + void(0); + ` + ); + + await BrowserTestUtils.browserLoaded(browser, true, function(url) { + info("URL:" + url); + return url == "http://example.com/"; + }); + + await SpecialPowers.spawn(browser, [], async function() { + Assert.ok(true, "origin " + content.document.nodePrincipal.origin); + + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "The principal of remote javascript: should be a NullPrincipal." + ); + + let str = content.document.nodePrincipal.originNoSuffix; + let expectDomain = + str.substring("moz-nullprincipal:{".length, str.length - 1) + ".mozilla"; + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + expectDomain, + "remote javascript: should have firstPartyDomain set to " + expectDomain + ); + + let iframe = content.document.getElementById("iframe1"); + await SpecialPowers.spawn(iframe, [expectDomain], function(domain) { + Assert.equal( + content.document.nodePrincipal.originAttributes.firstPartyDomain, + domain, + "iframe should have firstPartyDomain set to " + domain + ); + }); + }); + + win.close(); +}); diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js new file mode 100644 index 0000000000..5cfcf2d6bd --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js @@ -0,0 +1,327 @@ +/** + * Bug 1508355 - A test case for ensuring the saving channel has a correct first + * party domain when going through different "Save ... AS." + */ + +"use strict"; + +/* import-globals-from ../../../../../toolkit/content/tests/browser/common/mockTransfer.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +const TEST_FIRST_PARTY = "example.com"; +const TEST_ORIGIN = `http://${TEST_FIRST_PARTY}`; +const TEST_BASE_PATH = + "/browser/browser/components/originattributes/test/browser/"; +const TEST_PATH = `${TEST_BASE_PATH}file_saveAs.sjs`; +const TEST_PATH_VIDEO = `${TEST_BASE_PATH}file_thirdPartyChild.video.ogv`; +const TEST_PATH_IMAGE = `${TEST_BASE_PATH}file_favicon.png`; + +// For the "Save Page As" test, we will check the channel of the sub-resource +// within the page. In this case, it is a image. +const TEST_PATH_PAGE = `${TEST_BASE_PATH}file_favicon.png`; + +// For the "Save Frame As" test, we will check the channel of the sub-resource +// within the frame. In this case, it is a image. +const TEST_PATH_FRAME = `${TEST_BASE_PATH}file_favicon.png`; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +add_setup(async function() { + info("Setting the prefs."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstparty.isolate", true], + ["dom.security.https_first", false], + ], + }); + + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function() { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForObservingChannel(aURL, aFirstParty) { + return new Promise(resolve => { + let observer = (aSubject, aTopic) => { + if (aTopic === "http-on-modify-request") { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + + // Make sure this is the request which we want to check. + if (!httpChannel.URI.spec.endsWith(aURL)) { + return; + } + + info(`Checking loadInfo for URI: ${httpChannel.URI.spec}\n`); + is( + reqLoadInfo.originAttributes.firstPartyDomain, + aFirstParty, + "The loadInfo has correct first party domain" + ); + + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +function createPromiseForTransferComplete() { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function(downloadSuccess) { + ok(downloadSuccess, "File should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +async function doCommandForFrameType() { + info("Opening the frame sub-menu under the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let frameMenu = contextMenu.querySelector("#frame"); + let frameMenuPopup = frameMenu.menupopup; + let frameMenuPopupPromise = BrowserTestUtils.waitForEvent( + frameMenuPopup, + "popupshown" + ); + + frameMenu.openMenu(true); + await frameMenuPopupPromise; + + info("Triggering the save process."); + let saveFrameCommand = contextMenu.querySelector("#context-saveframe"); + frameMenuPopup.activateItem(saveFrameCommand); +} + +add_task(async function test_setup() { + // Make sure SearchService is ready for it to be called. + await Services.search.init(); +}); + +add_task(async function testContextMenuSaveAs() { + const TEST_DATA = [ + { type: "link", path: TEST_PATH, target: "#link1" }, + { type: "video", path: TEST_PATH_VIDEO, target: "#video1" }, + { type: "image", path: TEST_PATH_IMAGE, target: "#image1" }, + { type: "page", path: TEST_PATH_PAGE, target: "body" }, + { + type: "frame", + path: TEST_PATH_FRAME, + target: "body", + doCommandFunc: doCommandForFrameType, + }, + ]; + + for (const data of TEST_DATA) { + info(`Open a new tab for testing "Save ${data.type} as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `${TEST_ORIGIN}${TEST_PATH}?${data.type}=1` + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + let browser = gBrowser.selectedBrowser; + + if (data.type === "frame") { + browser = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => content.document.getElementById("frame1").browsingContext + ); + } + + info("Open the context menu."); + await BrowserTestUtils.synthesizeMouseAtCenter( + data.target, + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + data.path, + TEST_FIRST_PARTY + ); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + + // Select "Save As" option from context menu. + if (!data.doCommandFunc) { + let saveElement = document.getElementById(`context-save${data.type}`); + info("Triggering the save process."); + contextMenu.activateItem(saveElement); + } else { + await data.doCommandFunc(); + } + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Wait until the menu is closed."); + await popupHiddenPromise; + + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function testFileMenuSavePageAs() { + info(`Open a new tab for testing "Save Page AS" in the file menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `${TEST_ORIGIN}${TEST_PATH}?page=1` + ); + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + TEST_PATH_PAGE, + TEST_FIRST_PARTY + ); + + let menubar = document.getElementById("main-menubar"); + let filePopup = document.getElementById("menu_FilePopup"); + + // We only use the shortcut keys to open the file menu in Windows and Linux. + // Mac doesn't have a shortcut to only open the file menu. Instead, we directly + // trigger the save in MAC without any UI interactions. + if (Services.appinfo.OS !== "Darwin") { + let menubarActive = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarActive" + ); + EventUtils.synthesizeKey("KEY_F10"); + await menubarActive; + + let popupShownPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popupshown" + ); + // In window, it still needs one extra down key to open the file menu. + if (Services.appinfo.OS === "WINNT") { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await popupShownPromise; + } + + info("Triggering the save process."); + let fileSavePageAsElement = document.getElementById("menu_savePage"); + fileSavePageAsElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + // Close the file menu. + if (Services.appinfo.OS !== "Darwin") { + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popuphidden" + ); + filePopup.hidePopup(); + await popupHiddenPromise; + } + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testPageInfoMediaSaveAs() { + info( + `Open a new tab for testing "Save AS" in the media panel of the page info.` + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `${TEST_ORIGIN}${TEST_PATH}?pageinfo=1` + ); + + info("Open the media panel of the pageinfo."); + let pageInfo = BrowserPageInfo( + gBrowser.selectedBrowser.currentURI.spec, + "mediaTab" + ); + + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + let imageTree = pageInfo.document.getElementById("imagetree"); + let imageRowsNum = imageTree.view.rowCount; + + is(imageRowsNum, 2, "There should be two media items here."); + + for (let i = 0; i < imageRowsNum; i++) { + imageTree.view.selection.select(i); + imageTree.ensureRowIsVisible(i); + imageTree.focus(); + + let url = pageInfo.gImageView.data[i][0]; // COL_IMAGE_ADDRESS + info(`Start to save the media item with URL: ${url}`); + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + url, + TEST_FIRST_PARTY + ); + + info("Triggering the save process."); + let saveElement = pageInfo.document.getElementById("imagesaveasbutton"); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + } + + pageInfo.close(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/originattributes/test/browser/browser_httpauth.js b/browser/components/originattributes/test/browser/browser_httpauth.js new file mode 100644 index 0000000000..7e9a990a9d --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_httpauth.js @@ -0,0 +1,77 @@ +let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let authPromptModalType = Services.prefs.getIntPref( + "prompts.modalType.httpAuth" +); + +let commonDialogEnabled = + authPromptModalType === Services.prompt.MODAL_TYPE_WINDOW || + (authPromptModalType === Services.prompt.MODAL_TYPE_TAB && + Services.prefs.getBoolPref("prompts.tabChromePromptSubDialog")); + +let server = new HttpServer(); +server.registerPathHandler("/file.html", fileHandler); +server.start(-1); + +let BASE_URI = "http://localhost:" + server.identity.primaryPort; +let FILE_URI = BASE_URI + "/file.html"; + +let credentialQueue = []; + +// Ask the user agent for authorization. +function fileHandler(metadata, response) { + if (!metadata.hasHeader("Authorization")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="User Visible Realm"'); + return; + } + + // This will be "account:password" encoded in base64. + credentialQueue.push(metadata.getHeader("Authorization")); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = "<html><body></body></html>"; + response.bodyOutputStream.write(body, body.length); +} + +function onCommonDialogLoaded(subject) { + let dialog; + if (commonDialogEnabled) { + dialog = subject.Dialog; + } else { + let promptBox = + subject.ownerGlobal.gBrowser.selectedBrowser.tabModalPromptBox; + dialog = promptBox.getPrompt(subject).Dialog; + } + // Submit random account and password + dialog.ui.loginTextbox.setAttribute("value", Math.random()); + dialog.ui.password1Textbox.setAttribute("value", Math.random()); + dialog.ui.button0.click(); +} + +let authPromptTopic = commonDialogEnabled + ? "common-dialog-loaded" + : "tabmodal-dialog-loaded"; +Services.obs.addObserver(onCommonDialogLoaded, authPromptTopic); + +registerCleanupFunction(() => { + Services.obs.removeObserver(onCommonDialogLoaded, authPromptTopic); + server.stop(() => { + server = null; + }); +}); + +function getResult() { + // If two targets are isolated, they should get different credentials. + // Otherwise, the credentials will be cached and therefore the same. + return credentialQueue.shift(); +} + +async function doInit(aMode) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", false]], + }); +} + +IsolationTestTools.runTests(FILE_URI, getResult, null, doInit); diff --git a/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js new file mode 100644 index 0000000000..8de67f3bf4 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js @@ -0,0 +1,89 @@ +/* + * Bug 1264572 - A test case for image cache isolation. + */ + +requestLongerTimeout(2); + +let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const NUM_ISOLATION_LOADS = 2; +const NUM_CACHED_LOADS = 1; + +let gHits = 0; + +let server = new HttpServer(); +server.registerPathHandler("/image.png", imageHandler); +server.registerPathHandler("/file.html", fileHandler); +server.start(-1); + +// Disable rcwn to make cache behavior deterministic. +let rcwnEnabled = Services.prefs.getBoolPref("network.http.rcwn.enabled"); +Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + +registerCleanupFunction(() => { + Services.prefs.setBoolPref("network.http.rcwn.enabled", rcwnEnabled); + + server.stop(() => { + server = null; + }); +}); + +let BASE_URI = "http://localhost:" + server.identity.primaryPort; +let IMAGE_URI = BASE_URI + "/image.png"; +let FILE_URI = BASE_URI + "/file.html"; + +function imageHandler(metadata, response) { + info("XXX: loading image from server"); + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="; + response.bodyOutputStream.write(body, body.length); +} + +function fileHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = `<html><body><image src=${IMAGE_URI}></body></html>`; + response.bodyOutputStream.write(body, body.length); +} + +async function doBefore() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", false]], + }); + + // reset hit counter + info("XXX resetting gHits"); + gHits = 0; + info("XXX clearing image cache"); + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(true); + imageCache.clearCache(false); + info("XXX clearning network cache"); + Services.cache2.clear(); +} + +// the test function does nothing on purpose. +function doTest(aBrowser) { + return 0; +} + +// the check function +function doCheck(shouldIsolate, a, b) { + // if we're doing first party isolation and the image cache isolation is + // working, then gHits should be 2 because the image would have been loaded + // one per first party domain. if first party isolation is disabled, then + // gHits should be 1 since there would be one image load from the server and + // one load from the image cache. + info(`XXX check: gHits == ${gHits}, shouldIsolate == ${shouldIsolate}`); + return shouldIsolate + ? gHits == NUM_ISOLATION_LOADS + : gHits == NUM_CACHED_LOADS; +} + +IsolationTestTools.runTests(FILE_URI, doTest, doCheck, doBefore); diff --git a/browser/components/originattributes/test/browser/browser_localStorageIsolation.js b/browser/components/originattributes/test/browser/browser_localStorageIsolation.js new file mode 100644 index 0000000000..a6a06abf78 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_localStorageIsolation.js @@ -0,0 +1,33 @@ +/** + * Bug 1264567 - A test case for localStorage isolation. + */ + +const TEST_PAGE = + "http://mochi.test:8888/browser/browser/components/" + + "originattributes/test/browser/file_firstPartyBasic.html"; + +// Use a random key so we don't access it in later tests. +const key = Math.random().toString(); + +// IsolationTestTools flushes all preferences +// hence we explicitly pref off https-first mode +async function prefOffHttpsFirstMode() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); +} + +// Define the testing function +function doTest(aBrowser) { + return SpecialPowers.spawn(aBrowser, [key], function(contentKey) { + let value = content.localStorage.getItem(contentKey); + if (value === null) { + // No value is found, so we create one. + value = Math.random().toString(); + content.localStorage.setItem(contentKey, value); + } + return value; + }); +} + +IsolationTestTools.runTests(TEST_PAGE, doTest, null, prefOffHttpsFirstMode); diff --git a/browser/components/originattributes/test/browser/browser_permissions.js b/browser/components/originattributes/test/browser/browser_permissions.js new file mode 100644 index 0000000000..4c52f95ac8 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_permissions.js @@ -0,0 +1,91 @@ +/** + * Bug 1282655 - Test if site permissions are universal across origin attributes. + * + * This test is testing the cookie "permission" for a specific URI. + */ + +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +const TEST_PAGE = "https://example.net"; +const uri = Services.io.newURI(TEST_PAGE); + +async function disableCookies() { + Services.cookies.removeAll(); + PermissionTestUtils.add(uri, "cookie", Services.perms.DENY_ACTION); + + // A workaround for making this test working. In Bug 1330467, we separate the + // permissions between different firstPartyDomains, but not for the + // userContextID and the privateBrowsingId. So we need to manually add the + // permission for FPDs in order to make this test working. This test should be + // eventually removed once the permissions are isolated by OAs. + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + firstPartyDomain: "example.com", + }); + PermissionTestUtils.add(principal, "cookie", Services.perms.DENY_ACTION); + + principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + firstPartyDomain: "example.org", + }); + PermissionTestUtils.add(principal, "cookie", Services.perms.DENY_ACTION); +} + +async function ensureCookieNotSet(aBrowser) { + await SpecialPowers.spawn(aBrowser, [], async function() { + content.document.cookie = "key=value; SameSite=None; Secure;"; + Assert.equal( + content.document.cookie, + "", + "Setting/reading cookies should be disabled" + + " for this domain for all origin attribute combinations." + ); + }); +} + +IsolationTestTools.runTests( + TEST_PAGE, + ensureCookieNotSet, + () => true, + disableCookies +); + +async function enableCookies() { + Services.cookies.removeAll(); + PermissionTestUtils.add(uri, "cookie", Services.perms.ALLOW_ACTION); + + // A workaround for making this test working. + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + firstPartyDomain: "example.com", + }); + PermissionTestUtils.add(principal, "cookie", Services.perms.ALLOW_ACTION); + + principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + firstPartyDomain: "example.org", + }); + PermissionTestUtils.add(principal, "cookie", Services.perms.ALLOW_ACTION); +} + +async function ensureCookieSet(aBrowser) { + await SpecialPowers.spawn(aBrowser, [], function() { + content.document.cookie = "key=value; SameSite=None; Secure;"; + Assert.equal( + content.document.cookie, + "key=value", + "Setting/reading cookies should be" + + " enabled for this domain for all origin attribute combinations." + ); + }); +} + +IsolationTestTools.runTests( + TEST_PAGE, + ensureCookieSet, + () => true, + enableCookies +); + +registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + Services.cookies.removeAll(); +}); diff --git a/browser/components/originattributes/test/browser/browser_postMessage.js b/browser/components/originattributes/test/browser/browser_postMessage.js new file mode 100644 index 0000000000..9cfe8c44a2 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_postMessage.js @@ -0,0 +1,121 @@ +/** + * Bug 1492607 - Test for assuring that postMessage cannot go across OAs. + */ + +const FPD_ONE = "http://example.com"; +const FPD_TWO = "http://example.org"; + +const TEST_BASE = "/browser/browser/components/originattributes/test/browser/"; + +add_setup(async function() { + // Make sure first party isolation is enabled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstparty.isolate", true], + ["dom.security.https_first", false], + ], + }); +}); + +async function runTestWithOptions( + aDifferentFPD, + aStarTargetOrigin, + aBlockAcrossFPD +) { + let testPageURL = aDifferentFPD + ? FPD_ONE + TEST_BASE + "file_postMessage.html" + : FPD_TWO + TEST_BASE + "file_postMessage.html"; + + // Deciding the targetOrigin according to the test setting. + let targetOrigin; + if (aStarTargetOrigin) { + targetOrigin = "*"; + } else { + targetOrigin = aDifferentFPD ? FPD_ONE : FPD_TWO; + } + let senderURL = + FPD_TWO + TEST_BASE + `file_postMessageSender.html?${targetOrigin}`; + + // Open a tab to listen messages. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testPageURL); + + // Use window.open() in the tab to open the sender tab. The sender tab + // will send a message through postMessage to window.opener. + let senderTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + senderURL, + true + ); + SpecialPowers.spawn(tab.linkedBrowser, [senderURL], aSenderPath => { + content.open(aSenderPath, "_blank"); + }); + + // Wait and get the tab of the sender tab. + let senderTab = await senderTabPromise; + + // The postMessage should be blocked when the first parties are different with + // the following two cases. First, it is using a non-star target origin. + // Second, it is using the star target origin and the pref + // 'privacy.firstparty.isolate.block_post_message' is true. + let shouldBlock = aDifferentFPD && (!aStarTargetOrigin || aBlockAcrossFPD); + + await SpecialPowers.spawn(tab.linkedBrowser, [shouldBlock], async aValue => { + await new Promise(resolve => { + content.addEventListener("message", async function eventHandler(aEvent) { + if (aEvent.data === "Self") { + let display = content.document.getElementById("display"); + if (aValue) { + Assert.equal( + display.innerHTML, + "", + "It should not get a message from other OA." + ); + } else { + await ContentTaskUtils.waitForCondition( + () => display.innerHTML == "Message", + "Wait for message to arrive" + ); + Assert.equal( + display.innerHTML, + "Message", + "It should get a message from the same OA." + ); + } + + content.removeEventListener("message", eventHandler); + resolve(); + } + }); + + // Trigger the content to send a postMessage to itself. + content.document.getElementById("button").click(); + }); + }); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(senderTab); +} + +add_task(async function runTests() { + for (let useDifferentFPD of [true, false]) { + for (let useStarTargetOrigin of [true, false]) { + for (let enableBlocking of [true, false]) { + if (enableBlocking) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate.block_post_message", true]], + }); + } + + await runTestWithOptions( + useDifferentFPD, + useStarTargetOrigin, + enableBlocking + ); + + if (enableBlocking) { + await SpecialPowers.popPrefEnv(); + } + } + } + } +}); diff --git a/browser/components/originattributes/test/browser/browser_sanitize.js b/browser/components/originattributes/test/browser/browser_sanitize.js new file mode 100644 index 0000000000..625dd85c65 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_sanitize.js @@ -0,0 +1,94 @@ +/** + * Bug 1270338 - Add a mochitest to ensure Sanitizer clears data for all containers + */ + +if (SpecialPowers.useRemoteSubframes) { + requestLongerTimeout(4); +} + +const CC = Components.Constructor; + +const TEST_DOMAIN = "https://example.net/"; + +const { Sanitizer } = ChromeUtils.import("resource:///modules/Sanitizer.jsm"); + +async function setCookies(aBrowser) { + await SpecialPowers.spawn(aBrowser, [], function() { + content.document.cookie = "key=value"; + }); +} + +function cacheDataForContext(loadContextInfo) { + return new Promise(resolve => { + let cachedURIs = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cachedURIs.push(uri.asciiSpec); + }, + onCacheEntryVisitCompleted() { + resolve(cachedURIs); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); +} + +async function checkCookiesSanitized(aBrowser) { + await SpecialPowers.spawn(aBrowser, [], function() { + Assert.equal( + content.document.cookie, + "", + "Cookies of all origin attributes should be cleared." + ); + }); +} + +function checkCacheExists(aShouldExist) { + return async function() { + let loadContextInfos = [ + Services.loadContextInfo.default, + Services.loadContextInfo.custom(false, { userContextId: 1 }), + Services.loadContextInfo.custom(false, { userContextId: 2 }), + Services.loadContextInfo.custom(false, { + firstPartyDomain: "example.com", + }), + Services.loadContextInfo.custom(false, { + firstPartyDomain: "example.org", + }), + ]; + let i = 0; + for (let loadContextInfo of loadContextInfos) { + let cacheURIs = await cacheDataForContext(loadContextInfo); + is( + cacheURIs.includes(TEST_DOMAIN), + aShouldExist, + TEST_DOMAIN + + " should " + + (aShouldExist ? "not " : "") + + "be cached for all origin attributes." + + i++ + ); + } + }; +} + +add_setup(async function() { + Services.cache2.clear(); +}); + +// This will set the cookies and the cache. +IsolationTestTools.runTests(TEST_DOMAIN, setCookies, () => true); + +add_task(checkCacheExists(true)); + +add_task(async function sanitize() { + await Sanitizer.sanitize(["cookies", "cache"]); +}); + +add_task(checkCacheExists(false)); +IsolationTestTools.runTests(TEST_DOMAIN, checkCookiesSanitized, () => true); diff --git a/browser/components/originattributes/test/browser/browser_sharedworker.js b/browser/components/originattributes/test/browser/browser_sharedworker.js new file mode 100644 index 0000000000..4ab34ed8a9 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_sharedworker.js @@ -0,0 +1,31 @@ +/** + * Bug 1264593 - A test case for the shared worker by first party isolation. + */ + +const TEST_DOMAIN = "https://example.net/"; +const TEST_PATH = + TEST_DOMAIN + "browser/browser/components/originattributes/test/browser/"; +const TEST_PAGE = TEST_PATH + "file_sharedworker.html"; + +async function getResultFromSharedworker(aBrowser) { + let response = await SpecialPowers.spawn(aBrowser, [], async function() { + let worker = new content.SharedWorker( + "file_sharedworker.js", + "isolationSharedWorkerTest" + ); + + let result = await new content.Promise(resolve => { + worker.port.onmessage = function(e) { + // eslint-disable-next-line no-unsanitized/property + content.document.getElementById("display").innerHTML = e.data; + resolve(e.data); + }; + }); + + return result; + }); + + return response; +} + +IsolationTestTools.runTests(TEST_PAGE, getResultFromSharedworker); diff --git a/browser/components/originattributes/test/browser/browser_windowOpenerRestriction.js b/browser/components/originattributes/test/browser/browser_windowOpenerRestriction.js new file mode 100644 index 0000000000..b07eb88b19 --- /dev/null +++ b/browser/components/originattributes/test/browser/browser_windowOpenerRestriction.js @@ -0,0 +1,113 @@ +/** + * Bug 1339336 - A test case for testing pref 'privacy.firstparty.isolate.restrict_opener_access' + */ + +const CC = Components.Constructor; + +const FIRST_PARTY_OPENER = "example.com"; +const FIRST_PARTY_TARGET = "example.org"; +const OPENER_PAGE = + "https://" + + FIRST_PARTY_OPENER + + "/browser/browser/components/" + + "originattributes/test/browser/file_windowOpenerRestriction.html"; +const TARGET_PAGE = + "https://" + + FIRST_PARTY_TARGET + + "/browser/browser/components/" + + "originattributes/test/browser/file_windowOpenerRestrictionTarget.html"; + +async function testPref(aIsPrefEnabled) { + // Use a random key so we don't access it in later tests. + let cookieStr = + "key" + Math.random().toString() + "=" + Math.random().toString(); + + // Open the tab for the opener page. + let tab = BrowserTestUtils.addTab(gBrowser, OPENER_PAGE); + + // Select this tab and make sure its browser is loaded and focused. + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ cookieStr, page: TARGET_PAGE, isPrefEnabled: aIsPrefEnabled }], + async function(obj) { + // Acquire the iframe element. + let childFrame = content.document.getElementById("child"); + + // Insert a cookie into this iframe. + await SpecialPowers.spawn(childFrame, [obj.cookieStr], aCookieStr => { + content.document.cookie = aCookieStr + "; SameSite=None; Secure;"; + }); + + // Open the tab here and focus on it. + let openedPath = obj.page; + if (!obj.isPrefEnabled) { + // If the pref is not enabled, we pass the cookie value through the query string + // to tell the target page that it should check the cookie value. + openedPath += "?" + obj.cookieStr; + } + + // Issue the opener page to open the target page and focus on it. + content.openedWindow = content.open(openedPath); + content.openedWindow.focus(); + } + ); + + // Wait until the target page is loaded. + let targetBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + await BrowserTestUtils.browserLoaded(targetBrowser); + + // The target page will do the check and show the result through its title. + is( + targetBrowser.contentTitle, + "pass", + "The behavior of window.opener is correct." + ); + + // Close Tabs. + await SpecialPowers.spawn(browser, [], async function() { + content.openedWindow.close(); + }); + BrowserTestUtils.removeTab(tab); + + // Reset cookies + Services.cookies.removeAll(); +} + +add_task(async function runTests() { + let tests = [true, false]; + + // First, we test the scenario that the first party isolation is enabled. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate", true]], + }); + + for (let enabled of tests) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate.restrict_opener_access", enabled]], + }); + + await testPref(enabled); + } + + // Second, we test the scenario that the first party isolation is disabled. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate", false]], + }); + + for (let enabled of tests) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.firstparty.isolate.restrict_opener_access", enabled]], + }); + + // When first party isolation is disabled, this pref will not affect the behavior of + // window.opener. And the correct behavior here is to allow access since the iframe in + // the opener page has the same origin with the target page. + await testPref(false); + } +}); diff --git a/browser/components/originattributes/test/browser/dummy.html b/browser/components/originattributes/test/browser/dummy.html new file mode 100644 index 0000000000..1a87e28408 --- /dev/null +++ b/browser/components/originattributes/test/browser/dummy.html @@ -0,0 +1,9 @@ +<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/originattributes/test/browser/file_broadcastChannel.html b/browser/components/originattributes/test/browser/file_broadcastChannel.html new file mode 100644 index 0000000000..21dfc5eb8d --- /dev/null +++ b/browser/components/originattributes/test/browser/file_broadcastChannel.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Page broadcast channel creator for first party isolation</title> +</head> +<body> + <div id="display" style="white-space:pre; font-family:monospace; display:inline;"></div> + <iframe id="iframe" src="file_broadcastChanneliFrame.html"></iframe>> +<script type="text/javascript"> +let bc = new BroadcastChannel("testBroadcastChannel"); +bc.onmessage = function(e) { + // eslint-disable-next-line no-unsanitized/property + document.getElementById("display").innerHTML = e.data; +}; +</script> +</body> diff --git a/browser/components/originattributes/test/browser/file_broadcastChanneliFrame.html b/browser/components/originattributes/test/browser/file_broadcastChanneliFrame.html new file mode 100644 index 0000000000..6f4c484bc9 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_broadcastChanneliFrame.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Page broadcast channel responder for first party isolation</title> +</head> +<body> + <div id="display" style="white-space:pre; font-family:monospace; display:inline;"></div> +<script type="text/javascript"> +let bc = new BroadcastChannel("testBroadcastChannel"); +bc.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); +}; +</script> +</body> diff --git a/browser/components/originattributes/test/browser/file_cache.html b/browser/components/originattributes/test/browser/file_cache.html new file mode 100644 index 0000000000..24e09baefa --- /dev/null +++ b/browser/components/originattributes/test/browser/file_cache.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<head> + <link rel="icon" type="image/png" href="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png"> + <link rel="stylesheet" type="text/css" + href="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css"> + <link rel="preconnect" href="http://example.net"> + <style type="text/css"> + @font-face { + font-family: foo; + src: url("http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.font.woff") format('woff'); + } + body { font-family: foo } + </style> +</head> +<body> +<div>file_cache.html</div> + +<iframe src="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html"> +</iframe> + +<script src="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js"> +</script> + +<img src="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png"> + +<embed src="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png"> + +<object data="http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png" + type="image/png"></object> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_favicon.html b/browser/components/originattributes/test/browser/file_favicon.html new file mode 100644 index 0000000000..f294b47758 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <title>Favicon Test for originAttributes</title> + <link rel="icon" type="image/png" href="file_favicon.png" /> + </head> + <body> + Favicon!! + </body> +</html> diff --git a/browser/components/originattributes/test/browser/file_favicon.png b/browser/components/originattributes/test/browser/file_favicon.png Binary files differnew file mode 100644 index 0000000000..5535363c94 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon.png diff --git a/browser/components/originattributes/test/browser/file_favicon.png^headers^ b/browser/components/originattributes/test/browser/file_favicon.png^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon.png^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/browser/components/originattributes/test/browser/file_favicon_cache.html b/browser/components/originattributes/test/browser/file_favicon_cache.html new file mode 100644 index 0000000000..9e3e0114a1 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon_cache.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <title>Favicon Test for originAttributes</title> + <link rel="icon" type="image/png" href="https://example.net/browser/browser/components/originattributes/test/browser/file_favicon_cache.png" /> + </head> + <body> + Third Party Favicon!! + </body> +</html> diff --git a/browser/components/originattributes/test/browser/file_favicon_cache.png b/browser/components/originattributes/test/browser/file_favicon_cache.png Binary files differnew file mode 100644 index 0000000000..5535363c94 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon_cache.png diff --git a/browser/components/originattributes/test/browser/file_favicon_thirdParty.html b/browser/components/originattributes/test/browser/file_favicon_thirdParty.html new file mode 100644 index 0000000000..e50b0c3493 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_favicon_thirdParty.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <title>Favicon Test for originAttributes</title> + <link rel="icon" type="image/png" href="https://example.net/browser/browser/components/originattributes/test/browser/file_favicon.png" /> + </head> + <body> + Third Party Favicon!! + </body> +</html> diff --git a/browser/components/originattributes/test/browser/file_firstPartyBasic.html b/browser/components/originattributes/test/browser/file_firstPartyBasic.html new file mode 100644 index 0000000000..713187fb2c --- /dev/null +++ b/browser/components/originattributes/test/browser/file_firstPartyBasic.html @@ -0,0 +1,8 @@ +<html> + <head> + <meta charset="UTF-8"> + <title>First Party Isolation Tests</title> + </head> + <body> + </body> +</html> diff --git a/browser/components/originattributes/test/browser/file_postMessage.html b/browser/components/originattributes/test/browser/file_postMessage.html new file mode 100644 index 0000000000..91255d0364 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_postMessage.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Test page for window.postMessage</title> +</head> +<body> + <div id="display" style="white-space:pre; font-family:monospace; display:inline;"></div> + <input type="button" id="button" title="button"/> + <script> + + const TEST_PATH = "http://example.org/browser/browser/components/originattributes/test/browser/file_postMessageSender.html"; + + window.onload = () => { + window.addEventListener("message", aEvent => { + if (aEvent.data === "Message") { + document.getElementById("display").innerHTML = "Message"; + } + }); + + document.getElementById("button").onclick = () => { + window.postMessage("Self", "/"); + }; + }; + </script> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_postMessageSender.html b/browser/components/originattributes/test/browser/file_postMessageSender.html new file mode 100644 index 0000000000..ebdffb990f --- /dev/null +++ b/browser/components/originattributes/test/browser/file_postMessageSender.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Test page for always sending a message to its opener through postMessage</title> +</head> +<body> + <script> + window.onload = () => { + // The target origin is coming from the query string. + let targetOrigin = window.location.search.substring(1); + window.opener.postMessage("Message", targetOrigin); + }; + </script> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_saveAs.sjs b/browser/components/originattributes/test/browser/file_saveAs.sjs new file mode 100644 index 0000000000..6be46daa6a --- /dev/null +++ b/browser/components/originattributes/test/browser/file_saveAs.sjs @@ -0,0 +1,40 @@ +const HTTP_ORIGIN = "http://example.com"; +const SECOND_ORIGIN = "http://example.org"; +const URI_PATH = "/browser/browser/components/originattributes/test/browser/"; +const LINK_PATH = `${URI_PATH}file_saveAs.sjs`; +// Reusing existing ogv file for testing. +const VIDEO_PATH = `${URI_PATH}file_thirdPartyChild.video.ogv`; +// Reusing existing png file for testing. +const IMAGE_PATH = `${URI_PATH}file_favicon.png`; +const FRAME_PATH = `${SECOND_ORIGIN}${URI_PATH}file_saveAs.sjs?image=1`; + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(aRequest, aResponse) { + var params = new URLSearchParams(aRequest.queryString); + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + let contentBody = ""; + + if (params.has("link")) { + contentBody = `<a href="${LINK_PATH}" id="link1">this is a link</a>`; + } else if (params.has("video")) { + contentBody = `<video src="${VIDEO_PATH}" id="video1"> </video>`; + } else if (params.has("image")) { + contentBody = `<img src="${IMAGE_PATH}" id="image1">`; + } else if (params.has("page")) { + // We need at least one resource, like a img, a link or a script, to trigger + // downloading resources in "Save Page As". Otherwise, it will output the + // document directly without any network request. + contentBody = `<img src="${IMAGE_PATH}">`; + } else if (params.has("frame")) { + // Like "Save Page As", we need to put at least one resource in the frame. + // Here we also use a image. + contentBody = `<iframe src="${FRAME_PATH}" id="frame1"></iframe>`; + } else if (params.has("pageinfo")) { + contentBody = `<img src="${IMAGE_PATH}" id="image1"> + <video src="${VIDEO_PATH}" id="video1"> </video>`; + } + + aResponse.write(`<html><body>${contentBody}</body></html>`); +} diff --git a/browser/components/originattributes/test/browser/file_sharedworker.html b/browser/components/originattributes/test/browser/file_sharedworker.html new file mode 100644 index 0000000000..b9ff793bd5 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_sharedworker.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Page SharedWorker creator for first party isolation</title> +</head> +<body> +<div id="display" style="white-space:pre; font-family:monospace; display:inline;"></div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_sharedworker.js b/browser/components/originattributes/test/browser/file_sharedworker.js new file mode 100644 index 0000000000..1fb7b0b005 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_sharedworker.js @@ -0,0 +1,9 @@ +self.randomValue = Math.random(); + +/* eslint-env worker */ + +onconnect = function(e) { + let port = e.ports[0]; + port.postMessage(self.randomValue); + port.start(); +}; diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg b/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg Binary files differnew file mode 100644 index 0000000000..edda4e9128 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png b/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png Binary files differnew file mode 100644 index 0000000000..c5916f2897 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png b/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png Binary files differnew file mode 100644 index 0000000000..c5916f2897 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html new file mode 100644 index 0000000000..037901ad06 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.fetch.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.font.woff b/browser/components/originattributes/test/browser/file_thirdPartyChild.font.woff Binary files differnew file mode 100644 index 0000000000..acda4f3d9f --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.font.woff diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html new file mode 100644 index 0000000000..b047d5b412 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.html</div> +<script> + var xhr = new XMLHttpRequest(); + xhr.open("GET", "http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html", true); + xhr.send(); + var worker = new Worker("http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js"); + var sharedWorker = new SharedWorker("http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js"); + + fetch("file_thirdPartyChild.fetch.html", {cache: "force-cache"} ); + fetch(new Request("file_thirdPartyChild.request.html"), {cache: "force-cache"} ); +</script> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png b/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png Binary files differnew file mode 100644 index 0000000000..c5916f2897 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js b/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js new file mode 100644 index 0000000000..dbf8f83769 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js @@ -0,0 +1 @@ +// dummy script, to be called by self.importScripts(...) diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css b/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css new file mode 100644 index 0000000000..6f8f41b4a4 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css @@ -0,0 +1 @@ +/* Dummy CSS file, used by browser_cache.js. */ diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png b/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png Binary files differnew file mode 100644 index 0000000000..c5916f2897 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html new file mode 100644 index 0000000000..108ed2ffa5 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.request.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js b/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js new file mode 100644 index 0000000000..6ddf436c09 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js @@ -0,0 +1 @@ +// Dummy child script, used by browser_cache.js diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js b/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js new file mode 100644 index 0000000000..b262fa10a3 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js @@ -0,0 +1 @@ +// dummy file diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.track.vtt b/browser/components/originattributes/test/browser/file_thirdPartyChild.track.vtt new file mode 100644 index 0000000000..b37cb40e45 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.track.vtt @@ -0,0 +1,13 @@ +WEBVTT FILE + +1 +00:00:00.500 --> 00:00:02.000 D:vertical A:start +blah blah blah + +2 +00:00:02.500 --> 00:00:04.300 +this is a test + +3 +00:00:05.000 --> 00:00:07.000 +one more line diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv Binary files differnew file mode 100644 index 0000000000..68dee3cf2b --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html new file mode 100644 index 0000000000..47e42d1e58 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.worker.fetch.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js new file mode 100644 index 0000000000..38aff85c30 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js @@ -0,0 +1,20 @@ +var xhr = new XMLHttpRequest(); +xhr.open( + "GET", + "http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html", + true +); +xhr.send(); + +fetch( + "http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html", + { cache: "force-cache" } +); +var myRequest = new Request( + "http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html" +); +fetch(myRequest, { cache: "force-cache" }); + +self.importScripts( + "http://example.net/browser/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js" +); diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html new file mode 100644 index 0000000000..5b5c55bfeb --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.worker.request.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html new file mode 100644 index 0000000000..9fc107f375 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.worker.xhr.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html b/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html new file mode 100644 index 0000000000..f56e7b3c1f --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> +<!-- The child page, used by browser_cache.js --> +<body> +<div>thirdPartyChild.html</div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_windowOpenerRestriction.html b/browser/components/originattributes/test/browser/file_windowOpenerRestriction.html new file mode 100644 index 0000000000..8925a6ccf5 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_windowOpenerRestriction.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Test page for window.opener accessibility</title> +</head> +<body> + <iframe id="child" name="child" src="https://example.org/browser/browser/components/originattributes/test/browser/file_firstPartyBasic.html"></iframe> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/file_windowOpenerRestrictionTarget.html b/browser/components/originattributes/test/browser/file_windowOpenerRestrictionTarget.html new file mode 100644 index 0000000000..5a14834897 --- /dev/null +++ b/browser/components/originattributes/test/browser/file_windowOpenerRestrictionTarget.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>title not set</title> + <script> + // If the query string is given, we are expecting the window.opener can be accessed + // across different first party domains, so we will match the cookie value. + // Otherwise, the access of window.opener should be treated as cross-origin. + // Therefore, it should fail at this setting. + let openerRestriction = true; + let cookieValue; + if (window.location.search.length) { + cookieValue = window.location.search.substr(1); + openerRestriction = false; + } + + try { + let openerFrame = window.opener.frames.child; + let result = openerFrame.document.cookie === cookieValue; + if (result && !openerRestriction) { + document.title = "pass"; + } + } catch (e) { + if (openerRestriction) { + document.title = "pass"; + } + } + </script> +</head> +<body> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/head.js b/browser/components/originattributes/test/browser/head.js new file mode 100644 index 0000000000..9ce6991ca3 --- /dev/null +++ b/browser/components/originattributes/test/browser/head.js @@ -0,0 +1,462 @@ +/* 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"; + +const TEST_URL_PATH = + "/browser/browser/components/originattributes/test/browser/"; + +// The flags of test modes. +const TEST_MODE_FIRSTPARTY = 0; +const TEST_MODE_NO_ISOLATION = 1; +const TEST_MODE_CONTAINERS = 2; + +// The name of each mode. +const TEST_MODE_NAMES = ["first party isolation", "no isolation", "containers"]; + +// The frame types. +const TEST_TYPE_FRAME = 1; +const TEST_TYPE_IFRAME = 2; + +// The default frame setting. +const DEFAULT_FRAME_SETTING = [TEST_TYPE_IFRAME]; + +let gFirstPartyBasicPage = TEST_URL_PATH + "file_firstPartyBasic.html"; + +/** + * Add a tab for the given url with the specific user context id. + * + * @param aURL + * The url of the page. + * @param aUserContextId + * The user context id for this tab. + * + * @return tab - The tab object of this tab. + * browser - The browser object of this tab. + */ +async function openTabInUserContext(aURL, aUserContextId) { + info(`Start to open tab in specific userContextID: ${aUserContextId}.`); + let originAttributes = { + userContextId: aUserContextId, + }; + info("Create triggeringPrincipal."); + let triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal( + makeURI(aURL), + originAttributes + ); + // Open the tab in the correct userContextId. + info("Open the tab and wait for it to be loaded."); + let tab = BrowserTestUtils.addTab(gBrowser, aURL, { + userContextId: aUserContextId, + triggeringPrincipal, + }); + + // Select tab and make sure its browser is focused. + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + info("Finished tab opening."); + + return { tab, browser }; +} + +/** + * Add a tab for a page with the given first party domain. This page will have + * an iframe which is loaded with the given url by default or you could specify + * a frame setting to create nested frames. And this function will also modify + * the 'content' in the ContentTask to the target frame's window object. + * + * @param aURL + * The url of the iframe. + * @param aFirstPartyDomain + * The first party domain. + * @param aFrameSetting + * This setting controls how frames are organized within the page. The + * setting is an array of frame types, the first item indicates the + * frame type (iframe or frame) of the first layer of the frame structure, + * and the second item indicates the second layer, and so on. The aURL will + * be loaded at the deepest layer. This is optional. + * + * @return tab - The tab object of this tab. + * browser - The browser object of this tab. + */ +async function openTabInFirstParty( + aURL, + aFirstPartyDomain, + aFrameSetting = DEFAULT_FRAME_SETTING +) { + info(`Start to open tab under first party domain "${aFirstPartyDomain}".`); + // If the first party domain ends with '/', we remove it. + if (aFirstPartyDomain.endsWith("/")) { + aFirstPartyDomain = aFirstPartyDomain.slice(0, -1); + } + + let basicPageURL = aFirstPartyDomain + gFirstPartyBasicPage; + + // Open the tab for the basic first party page. + info("Open the tab and then wait for it to be loaded."); + let tab = BrowserTestUtils.addTab(gBrowser, basicPageURL); + + // Select tab and make sure its browser is focused. + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Clone the frame setting here since we will modify it later. + let frameSetting = aFrameSetting.slice(0); + let frameType; + let targetBrowsingContext; + + // Create the frame structure. + info("Create the frame structure."); + while ((frameType = frameSetting.shift())) { + if (!targetBrowsingContext) { + targetBrowsingContext = browser; + } + + let frameURL = !frameSetting.length ? aURL : basicPageURL; + + if (frameType == TEST_TYPE_FRAME) { + info("Add the frameset."); + targetBrowsingContext = await SpecialPowers.spawn( + targetBrowsingContext, + [frameURL], + async function(aFrameURL) { + // Add a frameset which carries the frame element. + let frameSet = content.document.createElement("frameset"); + frameSet.cols = "50%,50%"; + + let frame = content.document.createElement("frame"); + let dummyFrame = content.document.createElement("frame"); + + frameSet.appendChild(frame); + frameSet.appendChild(dummyFrame); + + content.document.body.appendChild(frameSet); + + // Wait for the frame to be loaded. + await new Promise(done => { + frame.addEventListener( + "load", + function() { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", aFrameURL); + }); + + return frame.browsingContext; + } + ); + } else if (frameType == TEST_TYPE_IFRAME) { + info("Add the iframe."); + targetBrowsingContext = await SpecialPowers.spawn( + targetBrowsingContext, + [frameURL], + async function(aFrameURL) { + // Add an iframe. + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + // Wait for the frame to be loaded. + await new Promise(done => { + frame.addEventListener( + "load", + function() { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", aFrameURL); + }); + + return frame.browsingContext; + } + ); + } else { + ok(false, "Invalid frame type."); + break; + } + info("Successfully added a frame"); + } + info("Finished the frame structure"); + + return { tab, browser: targetBrowsingContext }; +} + +this.IsolationTestTools = { + /** + * Adds isolation tests for first party isolation, no isolation + * and containers respectively. + * + * @param aTask + * The testing task which will be run in different settings. + */ + _add_task(aTask) { + let testSettings = [ + { + mode: TEST_MODE_FIRSTPARTY, + skip: false, + prefs: [["privacy.firstparty.isolate", true]], + }, + { + mode: TEST_MODE_NO_ISOLATION, + skip: false, + prefs: [["privacy.firstparty.isolate", false]], + }, + { + mode: TEST_MODE_CONTAINERS, + skip: false, + prefs: [["privacy.userContext.enabled", true]], + }, + ]; + + // Add test tasks. + for (let testSetting of testSettings) { + IsolationTestTools._addTaskForMode( + testSetting.mode, + testSetting.prefs, + testSetting.skip, + aTask + ); + } + }, + + _addTaskForMode(aMode, aPref, aSkip, aTask) { + if (aSkip) { + return; + } + + add_task(async function() { + info(`Starting the test for ${TEST_MODE_NAMES[aMode]}.`); + + // Before run this task, reset the preferences first. + await SpecialPowers.flushPrefEnv(); + + // Make sure preferences are set properly. + await SpecialPowers.pushPrefEnv({ set: aPref }); + + await SpecialPowers.pushPrefEnv({ set: [["dom.ipc.processCount", 1]] }); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true, + ], + ], + }); + + await aTask(aMode); + }); + }, + + /** + * Add a tab with the given tab setting, this will open different types of + * tabs according to the given test mode. A tab setting means a isolation + * target in different test mode; a tab setting indicates a first party + * domain when testing the first party isolation, it is a user context + * id when testing containers. + * + * @param aMode + * The test mode which decides what type of tabs will be opened. + * @param aURL + * The url which is going to open. + * @param aTabSettingObj + * The tab setting object includes 'firstPartyDomain' for the first party + * domain and 'userContextId' for Containers. + * @param aFrameSetting + * This setting controls how frames are organized within the page. The + * setting is an array of frame types, the first item indicates the + * frame type (iframe or frame) of the first layer of the frame structure, + * and the second item indicates the second layer, and so on. The aURL + * will be loaded at the deepest layer. This is optional. + * + * @return tab - The tab object of this tab. + * browser - The browser object of this tab. + */ + _addTab(aMode, aURL, aTabSettingObj, aFrameSetting) { + if (aMode === TEST_MODE_CONTAINERS) { + return openTabInUserContext(aURL, aTabSettingObj.userContextId); + } + + return openTabInFirstParty( + aURL, + aTabSettingObj.firstPartyDomain, + aFrameSetting + ); + }, + + /** + * Run isolation tests. The framework will run tests with standard combinations + * of prefs and tab settings, and checks whether the isolation is working. + * + * @param aURL + * The URL of the page that will be tested or an object contains 'url', + * the tested page, 'firstFrameSetting' for the frame setting of the first + * tab, and 'secondFrameSetting' for the second tab. + * @param aGetResultFuncs + * An array of functions or a single function which are responsible for + * returning the isolation result back to the framework for further checking. + * Each of these functions will be provided the browser object of the tab, + * that allows modifying or fetchings results from the page content. + * @param aCompareResultFunc + * An optional function which allows modifying the way how does framework + * check results. This function will be provided a boolean to indicate + * the isolation is no or off and two results. This function should return + * a boolean to tell that whether isolation is working. If this function + * is not given, the framework will take case checking by itself. + * @param aBeforeFunc + * An optional function which is called before any tabs are created so + * that the test case can set up/reset local state. + * @param aGetResultImmediately + * An optional boolean to ensure we get results before the next tab is opened. + */ + runTests( + aURL, + aGetResultFuncs, + aCompareResultFunc, + aBeforeFunc, + aGetResultImmediately, + aUseHttps + ) { + let pageURL; + let firstFrameSetting; + let secondFrameSetting; + + // Request a longer timeout since the test will run a test for three times + // with different settings. Thus, one test here represents three tests. + // For this reason, we triple the timeout. + requestLongerTimeout(3); + + if (typeof aURL === "string") { + pageURL = aURL; + } else if (typeof aURL === "object") { + pageURL = aURL.url; + firstFrameSetting = aURL.firstFrameSetting; + secondFrameSetting = aURL.secondFrameSetting; + } + + if (!Array.isArray(aGetResultFuncs)) { + aGetResultFuncs = [aGetResultFuncs]; + } + + let tabSettings = aUseHttps + ? [ + { firstPartyDomain: "https://example.com", userContextId: 1 }, + { firstPartyDomain: "https://example.org", userContextId: 2 }, + ] + : [ + { firstPartyDomain: "http://example.com", userContextId: 1 }, + { firstPartyDomain: "http://example.org", userContextId: 2 }, + ]; + + this._add_task(async function(aMode) { + let tabSettingA = 0; + + for (let tabSettingB of [0, 1]) { + // Give the test a chance to set up before each case is run. + if (aBeforeFunc) { + try { + await aBeforeFunc(aMode); + } catch (e) { + ok(false, `Caught error while doing testing setup: ${e}.`); + } + } + // Create Tabs. + info(`Create tab A for ${TEST_MODE_NAMES[aMode]} test.`); + let tabInfoA = await IsolationTestTools._addTab( + aMode, + pageURL, + tabSettings[tabSettingA], + firstFrameSetting + ); + info(`Finished Create tab A for ${TEST_MODE_NAMES[aMode]} test.`); + let resultsA = []; + if (aGetResultImmediately) { + try { + info( + `Immediately get result from tab A for ${TEST_MODE_NAMES[aMode]} test` + ); + for (let getResultFunc of aGetResultFuncs) { + resultsA.push(await getResultFunc(tabInfoA.browser)); + } + } catch (e) { + ok(false, `Caught error while getting result from Tab A: ${e}.`); + } + } + info(`Create tab B for ${TEST_MODE_NAMES[aMode]}.`); + let tabInfoB = await IsolationTestTools._addTab( + aMode, + pageURL, + tabSettings[tabSettingB], + secondFrameSetting + ); + info(`Finished Create tab B for ${TEST_MODE_NAMES[aMode]} test.`); + let i = 0; + for (let getResultFunc of aGetResultFuncs) { + // Fetch results from tabs. + info(`Fetching result from tab A for ${TEST_MODE_NAMES[aMode]}.`); + let resultA; + try { + resultA = aGetResultImmediately + ? resultsA[i++] + : await getResultFunc(tabInfoA.browser); + } catch (e) { + ok(false, `Caught error while getting result from Tab A: ${e}.`); + } + info(`Fetching result from tab B for ${TEST_MODE_NAMES[aMode]}.`); + let resultB; + try { + resultB = await getResultFunc(tabInfoB.browser); + } catch (e) { + ok(false, `Caught error while getting result from Tab B: ${e}.`); + } + // Compare results. + let result = false; + let shouldIsolate = + aMode !== TEST_MODE_NO_ISOLATION && tabSettingA !== tabSettingB; + if (aCompareResultFunc) { + result = await aCompareResultFunc(shouldIsolate, resultA, resultB); + } else { + result = shouldIsolate ? resultA !== resultB : resultA === resultB; + } + + let msg = + `Result of Testing ${TEST_MODE_NAMES[aMode]} for ` + + `isolation ${shouldIsolate ? "on" : "off"} with TabSettingA ` + + `${tabSettingA} and tabSettingB ${tabSettingB}` + + `, resultA = ${resultA}, resultB = ${resultB}`; + + ok(result, msg); + } + + // Close Tabs. + BrowserTestUtils.removeTab(tabInfoA.tab); + BrowserTestUtils.removeTab(tabInfoB.tab); + + // A workaround for avoiding a timing issue in Fission. This workaround + // makes sure that the shutdown process between parent and content + // is finished before the next round of testing. + 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"); + }); + } + } + }); + }, +}; diff --git a/browser/components/originattributes/test/browser/test.html b/browser/components/originattributes/test/browser/test.html new file mode 100644 index 0000000000..fe5c7a2cf7 --- /dev/null +++ b/browser/components/originattributes/test/browser/test.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script> + window.onmessage = function(evt) { + if (evt.data != "HI") { + return; + } + + window.parent.postMessage("OK", "https://example.net"); + }; + </script> +</head> +<body> + Hello World. +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test.js b/browser/components/originattributes/test/browser/test.js new file mode 100644 index 0000000000..d290af9b06 --- /dev/null +++ b/browser/components/originattributes/test/browser/test.js @@ -0,0 +1 @@ +var i = 1; diff --git a/browser/components/originattributes/test/browser/test.js^headers^ b/browser/components/originattributes/test/browser/test.js^headers^ new file mode 100644 index 0000000000..2ebf93751b --- /dev/null +++ b/browser/components/originattributes/test/browser/test.js^headers^ @@ -0,0 +1 @@ +Set-Cookie: test=foo; SameSite=None; Secure; diff --git a/browser/components/originattributes/test/browser/test2.html b/browser/components/originattributes/test/browser/test2.html new file mode 100644 index 0000000000..370be15600 --- /dev/null +++ b/browser/components/originattributes/test/browser/test2.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="test2.js"></script> +</head> +<body> + Hello World. +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test2.js b/browser/components/originattributes/test/browser/test2.js new file mode 100644 index 0000000000..d290af9b06 --- /dev/null +++ b/browser/components/originattributes/test/browser/test2.js @@ -0,0 +1 @@ +var i = 1; diff --git a/browser/components/originattributes/test/browser/test2.js^headers^ b/browser/components/originattributes/test/browser/test2.js^headers^ new file mode 100644 index 0000000000..0fbbf8c3eb --- /dev/null +++ b/browser/components/originattributes/test/browser/test2.js^headers^ @@ -0,0 +1 @@ +Set-Cookie: test2=foo; SameSite=None; Secure; diff --git a/browser/components/originattributes/test/browser/test_firstParty.html b/browser/components/originattributes/test/browser/test_firstParty.html new file mode 100644 index 0000000000..a2028ba663 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <div> + <iframe id="iframe1" src="https://example.com"></iframe> + <iframe id="iframe2" sandbox="" src="https://example.com"></iframe> + <iframe id="iframe3" sandbox="allow-same-origin" src="https://example.com"></iframe> + </div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_cookie.html b/browser/components/originattributes/test/browser/test_firstParty_cookie.html new file mode 100644 index 0000000000..44547c0d75 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_cookie.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="test.js"></script> +</head> +<body> + Hello World. + <iframe id="iframe1" src="test2.html"></iframe> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_html_redirect.html b/browser/components/originattributes/test/browser/test_firstParty_html_redirect.html new file mode 100644 index 0000000000..ac85b1d78a --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_html_redirect.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf8" http-equiv="refresh" content="0; url=https://example.com/"/> + <title>Test for Bug 1260931</title> +</head> +<body> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html b/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html new file mode 100644 index 0000000000..7b794a011f --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 1260931</title> +</head> +<body> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html^headers^ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html^headers^ new file mode 100644 index 0000000000..ba266f7c68 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: https://example.com/browser/browser/components/originattributes/test/browser/dummy.html diff --git a/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html b/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html new file mode 100644 index 0000000000..7b794a011f --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 1260931</title> +</head> +<body> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html^headers^ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html^headers^ new file mode 100644 index 0000000000..138d6102c1 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_http_redirect_to_same_domain.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: https://example.net/browser/browser/components/originattributes/test/browser/dummy.html diff --git a/browser/components/originattributes/test/browser/test_firstParty_iframe_http_redirect.html b/browser/components/originattributes/test/browser/test_firstParty_iframe_http_redirect.html new file mode 100644 index 0000000000..fd7df46c15 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_iframe_http_redirect.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <div> + <iframe id="iframe1" src="test_firstParty_http_redirect.html"></iframe> + </div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_firstParty_postMessage.html b/browser/components/originattributes/test/browser/test_firstParty_postMessage.html new file mode 100644 index 0000000000..a056909e37 --- /dev/null +++ b/browser/components/originattributes/test/browser/test_firstParty_postMessage.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 1260931</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function onload() { + let iframe1 = document.getElementById("iframe1"); + iframe1.contentWindow.postMessage("HI", "https://example.net"); +} + +window.onmessage = function(evt) { + document.getElementById("message").textContent = evt.data; + + let iframe2 = document.createElement("iframe"); + iframe2.src = "dummy.html"; + document.body.appendChild(iframe2); +}; +</script> +<body onload="onload()"> + <div> + <iframe id="iframe1" src="test.html"></iframe> + <span id="message"></span> + </div> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/test_form.html b/browser/components/originattributes/test/browser/test_form.html new file mode 100644 index 0000000000..db1b900e8b --- /dev/null +++ b/browser/components/originattributes/test/browser/test_form.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260931</title> +</head> +<body> +<form action="test_firstParty_http_redirect_to_same_domain.html" method="POST"> + First name: <input type="text" name="fname"><br> + Last name: <input type="text" name="lname"><br> + <input type="submit" id="submit" value="Submit"> +</form> +</body> +</html> diff --git a/browser/components/originattributes/test/browser/window.html b/browser/components/originattributes/test/browser/window.html new file mode 100644 index 0000000000..efcda61c10 --- /dev/null +++ b/browser/components/originattributes/test/browser/window.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> +<html> + <head> + <meta charset="utf8"> + <title>Page creating a popup</title> + </head> + <body> + <script type="text/javascript"> + </script> + </body> +</html> diff --git a/browser/components/originattributes/test/browser/window2.html b/browser/components/originattributes/test/browser/window2.html new file mode 100644 index 0000000000..955d2cebb0 --- /dev/null +++ b/browser/components/originattributes/test/browser/window2.html @@ -0,0 +1,11 @@ +<html> + <head> + <meta charset="utf8"> + <title>Page creating a popup</title> + </head> + <body> + <script type="text/javascript"> + var w = window.open("https://example.com/browser/browser/components/originattributes/test/browser/test_firstParty.html", "test"); + </script> + </body> +</html> diff --git a/browser/components/originattributes/test/browser/window3.html b/browser/components/originattributes/test/browser/window3.html new file mode 100644 index 0000000000..92b473aa9d --- /dev/null +++ b/browser/components/originattributes/test/browser/window3.html @@ -0,0 +1,11 @@ +<html> + <head> + <meta charset="utf8"> + <title>Page creating a popup</title> + </head> + <body> + <script type="text/javascript"> + var w = window.open("https://example.com/browser/browser/components/originattributes/test/browser/test_form.html", "test"); + </script> + </body> +</html> diff --git a/browser/components/originattributes/test/browser/window_redirect.html b/browser/components/originattributes/test/browser/window_redirect.html new file mode 100644 index 0000000000..37beed9101 --- /dev/null +++ b/browser/components/originattributes/test/browser/window_redirect.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> +<html> + <head> + <meta charset="utf8"> + <title>Page creating a popup</title> + </head> + <body> + <script type="text/javascript"> + var w = window.open("test_firstParty_http_redirect_to_same_domain.html", "test"); + </script> + </body> +</html> diff --git a/browser/components/originattributes/test/browser/worker_blobify.js b/browser/components/originattributes/test/browser/worker_blobify.js new file mode 100644 index 0000000000..5d84957c8f --- /dev/null +++ b/browser/components/originattributes/test/browser/worker_blobify.js @@ -0,0 +1,49 @@ +// Wait for a string to be posted to this worker. +// Create a blob containing this string, and then +// post back a blob URL pointing to the blob. + +/* eslint-env worker */ + +var postStringInBlob = function(blobObject) { + var fileReader = new FileReaderSync(); + var result = fileReader.readAsText(blobObject); + postMessage(result); +}; + +self.addEventListener("message", e => { + if (e.data.what === "blobify") { + try { + let blobURL = URL.createObjectURL(new Blob([e.data.message])); + postMessage({ blobURL }); + } catch (ex) { + postMessage({ error: ex.message }); + } + return; + } + + if (e.data.what === "deblobify") { + if ("error" in e.data.message) { + postMessage(e.data.message); + return; + } + let blobURL = e.data.message.blobURL, + xhr = new XMLHttpRequest(); + try { + xhr.open("GET", blobURL, true); + xhr.onload = function() { + postStringInBlob(xhr.response); + }; + xhr.onerror = function() { + postMessage({ error: "xhr error" }); + }; + xhr.responseType = "blob"; + xhr.send(); + } catch (ex) { + postMessage({ error: ex.message }); + } + + return; + } + + postMessage("Invalid operation!"); +}); diff --git a/browser/components/originattributes/test/mochitest/file_empty.html b/browser/components/originattributes/test/mochitest/file_empty.html new file mode 100644 index 0000000000..15648ec5aa --- /dev/null +++ b/browser/components/originattributes/test/mochitest/file_empty.html @@ -0,0 +1,2 @@ +<h1>I'm just a support file</h1> +<p>I get loaded to do permission testing.</p> diff --git a/browser/components/originattributes/test/mochitest/mochitest.ini b/browser/components/originattributes/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..459f5a984a --- /dev/null +++ b/browser/components/originattributes/test/mochitest/mochitest.ini @@ -0,0 +1,7 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +support-files = + file_empty.html + +[test_permissions_api.html] +skip-if = xorigin # Hangs diff --git a/browser/components/originattributes/test/mochitest/test_permissions_api.html b/browser/components/originattributes/test/mochitest/test_permissions_api.html new file mode 100644 index 0000000000..30f218b2c9 --- /dev/null +++ b/browser/components/originattributes/test/mochitest/test_permissions_api.html @@ -0,0 +1,211 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Test for Permissions API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <pre id="test"></pre> + <script type="application/javascript"> + "use strict"; + + const { + UNKNOWN_ACTION, + PROMPT_ACTION, + ALLOW_ACTION, + DENY_ACTION, + } = SpecialPowers.Ci.nsIPermissionManager; + + SimpleTest.waitForExplicitFinish(); + + const PERMISSIONS = [{ + name: "geolocation", + type: "geo", + }, { + name: "notifications", + type: "desktop-notification", + }, { + name: "push", + type: "desktop-notification", + }, { + name: "persistent-storage", + type: "persistent-storage", + }, { + name: "midi", + type: "midi", + } ]; + + const UNSUPPORTED_PERMISSIONS = [ + "foobarbaz", // Not in spec, for testing only. + ]; + + // Create a closure, so that tests are run on the correct window object. + function createPermissionTester(aWindow) { + return { + setPermissions(allow) { + const permissions = PERMISSIONS.map(({ type }) => { + return { + type, + allow, + "context": aWindow.document, + }; + }); + return new Promise((resolve) => { + SpecialPowers.popPermissions(() => { + SpecialPowers.pushPermissions(permissions, resolve); + }); + }); + }, + revokePermissions() { + const promisesToRevoke = PERMISSIONS.map(({ name }) => { + return aWindow.navigator.permissions + .revoke({ name }) + .then( + ({ state }) => is(state, "prompt", `correct state for '${name}'`), + () => ok(false, `revoke should not have rejected for '${name}'`) + ); + }); + return Promise.all(promisesToRevoke); + }, + revokeUnsupportedPermissions() { + const promisesToRevoke = UNSUPPORTED_PERMISSIONS.map(({ name }) => { + return aWindow.navigator.permissions + .revoke({ name }) + .then( + () => ok(false, `revoke should not have resolved for '${name}'`), + error => is(error.name, "TypeError", `revoke should have thrown TypeError for '${name}'`) + ); + }); + return Promise.all(promisesToRevoke); + }, + checkPermissions(state) { + const promisesToQuery = PERMISSIONS.map(({ name }) => { + return aWindow.navigator.permissions + .query({ name }) + .then( + () => is(state, state, `correct state for '${name}'`), + () => ok(false, `query should not have rejected for '${name}'`) + ); + }); + return Promise.all(promisesToQuery); + }, + checkUnsupportedPermissions() { + const promisesToQuery = UNSUPPORTED_PERMISSIONS.map(({ name }) => { + return aWindow.navigator.permissions + .query({ name }) + .then( + () => ok(false, `query should not have resolved for '${name}'`), + error => { + is(error.name, "TypeError", + `query should have thrown TypeError for '${name}'`); + } + ); + }); + return Promise.all(promisesToQuery); + }, + promiseStateChanged(name, state) { + return aWindow.navigator.permissions + .query({ name }) + .then(status => { + return new Promise( resolve => { + status.onchange = () => { + status.onchange = null; + is(status.state, state, `state changed for '${name}'`); + resolve(); + }; + }); + }, + () => ok(false, `query should not have rejected for '${name}'`)); + }, + testStatusOnChange() { + return new Promise((resolve) => { + SpecialPowers.popPermissions(() => { + const permission = "geolocation"; + const promiseGranted = this.promiseStateChanged(permission, "granted"); + this.setPermissions(ALLOW_ACTION); + promiseGranted.then(async () => { + const promisePrompt = this.promiseStateChanged(permission, "prompt"); + await SpecialPowers.popPermissions(); + return promisePrompt; + }).then(resolve); + }); + }); + }, + testInvalidQuery() { + return aWindow.navigator.permissions + .query({ name: "invalid" }) + .then( + () => ok(false, "invalid query should not have resolved"), + () => ok(true, "invalid query should have rejected") + ); + }, + testInvalidRevoke() { + return aWindow.navigator.permissions + .revoke({ name: "invalid" }) + .then( + () => ok(false, "invalid revoke should not have resolved"), + () => ok(true, "invalid revoke should have rejected") + ); + }, + }; + } + + function enablePrefs() { + const ops = { + "set": [ + ["dom.permissions.revoke.enable", true], + ["privacy.firstparty.isolate", true], + ], + }; + return SpecialPowers.pushPrefEnv(ops); + } + + function createIframe() { + return new Promise((resolve) => { + const iframe = document.createElement("iframe"); + iframe.src = "file_empty.html"; + iframe.onload = () => resolve(iframe.contentWindow); + document.body.appendChild(iframe); + }); + } + + window.onload = () => { + enablePrefs() + .then(createIframe) + .then(createPermissionTester) + .then((tester) => { + return tester + .checkUnsupportedPermissions() + .then(() => tester.setPermissions(UNKNOWN_ACTION)) + .then(() => tester.checkPermissions("prompt")) + .then(() => tester.setPermissions(PROMPT_ACTION)) + .then(() => tester.checkPermissions("prompt")) + .then(() => tester.setPermissions(ALLOW_ACTION)) + .then(() => tester.checkPermissions("granted")) + .then(() => tester.setPermissions(DENY_ACTION)) + .then(() => tester.checkPermissions("denied")) + .then(() => tester.testStatusOnChange()) + .then(() => tester.testInvalidQuery()) + .then(() => tester.revokeUnsupportedPermissions()) + .then(() => tester.revokePermissions()) + .then(() => tester.checkPermissions("prompt")) + .then(() => tester.testInvalidRevoke()); + }) + .then(SimpleTest.finish) + .catch((e) => { + ok(false, `Unexpected error ${e}`); + SimpleTest.finish(); + }); + }; + </script> +</body> + +</html> |