diff options
Diffstat (limited to '')
7 files changed, 1114 insertions, 0 deletions
diff --git a/devtools/shared/commands/inspected-window/inspected-window-command.js b/devtools/shared/commands/inspected-window/inspected-window-command.js new file mode 100644 index 0000000000..0d15016ebd --- /dev/null +++ b/devtools/shared/commands/inspected-window/inspected-window-command.js @@ -0,0 +1,145 @@ +/* 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 { + getAdHocFrontOrPrimitiveGrip, + // eslint-disable-next-line mozilla/reject-some-requires +} = require("resource://devtools/client/fronts/object.js"); + +/** + * For now, this class is mostly a wrapper around webExtInspectedWindow actor. + */ +class InspectedWindowCommand { + constructor({ commands }) { + this.commands = commands; + } + + /** + * Return a promise that resolves to the related target actor's front. + * The Web Extension inspected window actor. + * + * @return {Promise<WebExtensionInspectedWindowFront>} + */ + getFront() { + return this.commands.targetCommand.targetFront.getFront( + "webExtensionInspectedWindow" + ); + } + + /** + * Evaluate the provided javascript code in a target window. + * + * @param {Object} webExtensionCallerInfo - The addonId and the url (the addon base url + * or the url of the actual caller filename and lineNumber) used to log useful + * debugging information in the produced error logs and eval stack trace. + * @param {String} expression - The expression to evaluate. + * @param {Object} options - An option object. Check the actor method definition to see + * what properties it can hold (minus the `consoleFront` property which is defined + * below). + * @param {WebConsoleFront} options.consoleFront - An optional webconsole front. When + * set, the result will be either a primitive, a LongStringFront or an + * ObjectFront, and the WebConsoleActor corresponding to the console front will + * be used to generate those, which is needed if we want to handle ObjectFronts + * on the client. + */ + async eval(webExtensionCallerInfo, expression, options = {}) { + const { consoleFront } = options; + + if (consoleFront) { + options.evalResultAsGrip = true; + options.toolboxConsoleActorID = consoleFront.actor; + delete options.consoleFront; + } + + const front = await this.getFront(); + const response = await front.eval( + webExtensionCallerInfo, + expression, + options + ); + + // If no consoleFront was provided, we can directly return the response. + if (!consoleFront) { + return response; + } + + if ( + !response.hasOwnProperty("exceptionInfo") && + !response.hasOwnProperty("valueGrip") + ) { + throw new Error( + "Response does not have `exceptionInfo` or `valueGrip` property" + ); + } + + if (response.exceptionInfo) { + console.error( + response.exceptionInfo.description, + ...(response.exceptionInfo.details || []) + ); + return response; + } + + // On the server, the valueGrip is created from the toolbox webconsole actor. + // If we want since the ObjectFront connection is inherited from the parent front, we + // need to set the console front as the parent front. + return getAdHocFrontOrPrimitiveGrip( + response.valueGrip, + consoleFront || this + ); + } + + /** + * Reload the target tab, optionally bypass cache, customize the userAgent and/or + * inject a script in targeted document or any of its sub-frame. + * + * @param {WebExtensionCallerInfo} callerInfo + * the addonId and the url (the addon base url or the url of the actual caller + * filename and lineNumber) used to log useful debugging information in the + * produced error logs and eval stack trace. + * @param {Object} options + * @param {boolean|undefined} options.ignoreCache + * Enable/disable the cache bypass headers. + * @param {string|undefined} options.injectedScript + * Evaluate the provided javascript code in the top level and every sub-frame + * created during the page reload, before any other script in the page has been + * executed. + * @param {string|undefined} options.userAgent + * Customize the userAgent during the page reload. + * @returns {Promise} A promise that resolves once the page is done loading when userAgent + * or injectedScript option are passed. If those options are not provided, the + * Promise will resolve after the reload was initiated. + */ + async reload(callerInfo, options = {}) { + if (this._reloadPending) { + return null; + } + + this._reloadPending = true; + + try { + // We always want to update the target configuration to set the user agent if one is + // passed, or to reset a potential existing override if userAgent isn't defined. + await this.commands.targetConfigurationCommand.updateConfiguration({ + customUserAgent: options.userAgent, + }); + + const front = await this.getFront(); + const result = await front.reload(callerInfo, options); + this._reloadPending = false; + + return result; + } catch (e) { + this._reloadPending = false; + console.error(e); + return Promise.reject({ + message: "An unexpected error occurred", + }); + } + } +} + +module.exports = InspectedWindowCommand; diff --git a/devtools/shared/commands/inspected-window/moz.build b/devtools/shared/commands/inspected-window/moz.build new file mode 100644 index 0000000000..2cf831d7b0 --- /dev/null +++ b/devtools/shared/commands/inspected-window/moz.build @@ -0,0 +1,10 @@ +# 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/. + +DevToolsModules( + "inspected-window-command.js", +) + +if CONFIG["MOZ_BUILD_APP"] != "mobile/android": + BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] diff --git a/devtools/shared/commands/inspected-window/tests/browser.ini b/devtools/shared/commands/inspected-window/tests/browser.ini new file mode 100644 index 0000000000..d3b5ce5fbc --- /dev/null +++ b/devtools/shared/commands/inspected-window/tests/browser.ini @@ -0,0 +1,20 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + !/devtools/client/shared/test/highlighter-test-actor.js + head.js + inspectedwindow-reload-target.sjs + +prefs = + # restrictedDomains must be set as early as possible, before the first use of + # the preference. browser_webextension_inspected_window_access.js relies on + # this pref to be set. We cannot use "prefs =" at the individual file, because + # another test in this manifest may already have resulted in browser startup. + extensions.webextensions.restrictedDomains=test2.example.com + +[browser_webextension_inspected_window.js] +[browser_webextension_inspected_window_access.js] +skip-if = http3
\ No newline at end of file diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js new file mode 100644 index 0000000000..bf2b752e4d --- /dev/null +++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window.js @@ -0,0 +1,523 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_RELOAD_URL = `${URL_ROOT_SSL}/inspectedwindow-reload-target.sjs`; + +async function setup(pageUrl) { + // Disable bfcache for Fission for now. + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", false]], + }); + + const extension = ExtensionTestUtils.loadExtension({ + background() { + // This is just an empty extension used to ensure that the caller extension uuid + // actually exists. + }, + }); + + await extension.startup(); + + const fakeExtCallerInfo = { + url: WebExtensionPolicy.getByID(extension.id).getURL( + "fake-caller-script.js" + ), + lineNumber: 1, + addonId: extension.id, + }; + + const tab = await addTab(pageUrl); + + const commands = await CommandsFactory.forTab(tab, { isWebExtension: true }); + await commands.targetCommand.startListening(); + + const webConsoleFront = await commands.targetCommand.targetFront.getFront( + "console" + ); + + return { + webConsoleFront, + commands, + extension, + fakeExtCallerInfo, + }; +} + +async function teardown({ commands, extension }) { + await commands.destroy(); + gBrowser.removeCurrentTab(); + await extension.unload(); +} + +function waitForNextTabNavigated(commands) { + const target = commands.targetCommand.targetFront; + return new Promise(resolve => { + target.on("tabNavigated", function tabNavigatedListener(pkt) { + if (pkt.state == "stop" && !pkt.isFrameSwitching) { + target.off("tabNavigated", tabNavigatedListener); + resolve(); + } + }); + }); +} + +// Script used as the injectedScript option in the inspectedWindow.reload tests. +function injectedScript() { + if (!window.pageScriptExecutedFirst) { + window.addEventListener( + "DOMContentLoaded", + function () { + if (document.querySelector("pre")) { + document.querySelector("pre").textContent = + "injected script executed first"; + } + }, + { once: true } + ); + } +} + +// Script evaluated in the target tab, to collect the results of injectedScript +// evaluation in the inspectedWindow.reload tests. +function collectEvalResults() { + const results = []; + let iframeDoc = document; + + while (iframeDoc) { + if (iframeDoc.querySelector("pre")) { + results.push(iframeDoc.querySelector("pre").textContent); + } + const iframe = iframeDoc.querySelector("iframe"); + iframeDoc = iframe ? iframe.contentDocument : null; + } + return JSON.stringify(results); +} + +add_task(async function test_successfull_inspectedWindowEval_result() { + const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL); + + const result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "window.location", + {} + ); + + ok(result.value, "Got a result from inspectedWindow eval"); + is( + result.value.href, + URL_ROOT_SSL, + "Got the expected window.location.href property value" + ); + is( + result.value.protocol, + "https:", + "Got the expected window.location.protocol property value" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_successfull_inspectedWindowEval_resultAsGrip() { + const { commands, extension, fakeExtCallerInfo, webConsoleFront } = + await setup(URL_ROOT_SSL); + + let result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "window", + { + evalResultAsGrip: true, + toolboxConsoleActorID: webConsoleFront.actor, + } + ); + + ok(result.valueGrip, "Got a result from inspectedWindow eval"); + ok(result.valueGrip.actor, "Got a object actor as expected"); + is(result.valueGrip.type, "object", "Got a value grip of type object"); + is( + result.valueGrip.class, + "Window", + "Got a value grip which is instanceof Location" + ); + + // Test invalid evalResultAsGrip request. + result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "window", + { + evalResultAsGrip: true, + } + ); + + ok( + !result.value && !result.valueGrip, + "Got a null result from the invalid inspectedWindow eval call" + ); + ok( + result.exceptionInfo.isError, + "Got an API Error result from inspectedWindow eval" + ); + ok( + !result.exceptionInfo.isException, + "An error isException is false as expected" + ); + is( + result.exceptionInfo.code, + "E_PROTOCOLERROR", + "Got the expected 'code' property in the error result" + ); + is( + result.exceptionInfo.description, + "Inspector protocol error: %s - %s", + "Got the expected 'description' property in the error result" + ); + is( + result.exceptionInfo.details.length, + 2, + "The 'details' array property should contains 1 element" + ); + is( + result.exceptionInfo.details[0], + "Unexpected invalid sidebar panel expression request", + "Got the expected content in the error results's details" + ); + is( + result.exceptionInfo.details[1], + "missing toolboxConsoleActorID", + "Got the expected content in the error results's details" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_error_inspectedWindowEval_result() { + const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL); + + const result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "window", + {} + ); + + ok(!result.value, "Got a null result from inspectedWindow eval"); + ok( + result.exceptionInfo.isError, + "Got an API Error result from inspectedWindow eval" + ); + ok( + !result.exceptionInfo.isException, + "An error isException is false as expected" + ); + is( + result.exceptionInfo.code, + "E_PROTOCOLERROR", + "Got the expected 'code' property in the error result" + ); + is( + result.exceptionInfo.description, + "Inspector protocol error: %s", + "Got the expected 'description' property in the error result" + ); + is( + result.exceptionInfo.details.length, + 1, + "The 'details' array property should contains 1 element" + ); + ok( + result.exceptionInfo.details[0].includes("cyclic object value"), + "Got the expected content in the error results's details" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowEval_result() { + const { commands, extension, fakeExtCallerInfo } = await setup(URL_ROOT_SSL); + + const result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "throw Error('fake eval error');", + {} + ); + + ok(result.exceptionInfo.isException, "Got an exception as expected"); + ok(!result.value, "Got an undefined eval value"); + ok(!result.exceptionInfo.isError, "An exception should not be isError=true"); + ok( + result.exceptionInfo.value.includes("Error: fake eval error"), + "Got the expected exception message" + ); + + const expectedCallerInfo = `called from ${fakeExtCallerInfo.url}:${fakeExtCallerInfo.lineNumber}`; + ok( + result.exceptionInfo.value.includes(expectedCallerInfo), + "Got the expected caller info in the exception message" + ); + + const expectedStack = `eval code:1:7`; + ok( + result.exceptionInfo.value.includes(expectedStack), + "Got the expected stack trace in the exception message" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowReload() { + const { commands, extension, fakeExtCallerInfo } = await setup( + `${TEST_RELOAD_URL}?test=cache` + ); + + // Test reload with bypassCache=false. + + const waitForNoBypassCacheReload = waitForNextTabNavigated(commands); + const reloadResult = await commands.inspectedWindowCommand.reload( + fakeExtCallerInfo, + { + ignoreCache: false, + } + ); + + ok( + !reloadResult, + "Got the expected undefined result from inspectedWindow reload" + ); + + await waitForNoBypassCacheReload; + + const noBypassCacheEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + noBypassCacheEval.result, + "empty cache headers", + "Got the expected result with reload forceBypassCache=false" + ); + + // Test reload with bypassCache=true. + + const waitForForceBypassCacheReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + ignoreCache: true, + }); + + await waitForForceBypassCacheReload; + + const forceBypassCacheEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + forceBypassCacheEval.result, + "no-cache:no-cache", + "Got the expected result with reload forceBypassCache=true" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowReload_customUserAgent() { + const { commands, extension, fakeExtCallerInfo } = await setup( + `${TEST_RELOAD_URL}?test=user-agent` + ); + + // Test reload with custom userAgent. + + const waitForCustomUserAgentReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + userAgent: "Customized User Agent", + }); + + await waitForCustomUserAgentReload; + + const customUserAgentEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + customUserAgentEval.result, + "Customized User Agent", + "Got the expected result on reload with a customized userAgent" + ); + + // Test reload with no custom userAgent. + + const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {}); + + await waitForNoCustomUserAgentReload; + + const noCustomUserAgentEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + noCustomUserAgentEval.result, + window.navigator.userAgent, + "Got the expected result with reload without a customized userAgent" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowReload_injectedScript() { + const { commands, extension, fakeExtCallerInfo } = await setup( + `${TEST_RELOAD_URL}?test=injected-script&frames=3` + ); + + // Test reload with an injectedScript. + + const waitForInjectedScriptReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + injectedScript: `new ${injectedScript}`, + }); + await waitForInjectedScriptReload; + + const injectedScriptEval = await commands.scriptCommand.execute( + `(${collectEvalResults})()` + ); + + const expectedResult = new Array(5).fill("injected script executed first"); + + SimpleTest.isDeeply( + JSON.parse(injectedScriptEval.result), + expectedResult, + "Got the expected result on reload with an injected script" + ); + + // Test reload without an injectedScript. + + const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {}); + await waitForNoInjectedScriptReload; + + const noInjectedScriptEval = await commands.scriptCommand.execute( + `(${collectEvalResults})()` + ); + + const newExpectedResult = new Array(5).fill("injected script NOT executed"); + + SimpleTest.isDeeply( + JSON.parse(noInjectedScriptEval.result), + newExpectedResult, + "Got the expected result on reload with no injected script" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowReload_multiple_calls() { + const { commands, extension, fakeExtCallerInfo } = await setup( + `${TEST_RELOAD_URL}?test=user-agent` + ); + + // Test reload with custom userAgent three times (and then + // check that only the first one has affected the page reload. + + const waitForCustomUserAgentReload = waitForNextTabNavigated(commands); + + commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + userAgent: "Customized User Agent 1", + }); + commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + userAgent: "Customized User Agent 2", + }); + + await waitForCustomUserAgentReload; + + const customUserAgentEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + customUserAgentEval.result, + "Customized User Agent 1", + "Got the expected result on reload with a customized userAgent" + ); + + // Test reload with no custom userAgent. + + const waitForNoCustomUserAgentReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {}); + + await waitForNoCustomUserAgentReload; + + const noCustomUserAgentEval = await commands.scriptCommand.execute( + "document.body.textContent" + ); + + is( + noCustomUserAgentEval.result, + window.navigator.userAgent, + "Got the expected result with reload without a customized userAgent" + ); + + await teardown({ commands, extension }); +}); + +add_task(async function test_exception_inspectedWindowReload_stopped() { + const { commands, extension, fakeExtCallerInfo } = await setup( + `${TEST_RELOAD_URL}?test=injected-script&frames=3` + ); + + // Test reload on a page that calls window.stop() immediately during the page loading + + const waitForPageLoad = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + "window.location += '&stop=windowStop'" + ); + + info("Load a webpage that calls 'window.stop()' while is still loading"); + await waitForPageLoad; + + info("Starting a reload with an injectedScript"); + const waitForInjectedScriptReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, { + injectedScript: `new ${injectedScript}`, + }); + await waitForInjectedScriptReload; + + const injectedScriptEval = await commands.scriptCommand.execute( + `(${collectEvalResults})()` + ); + + // The page should have stopped during the reload and only one injected script + // is expected. + const expectedResult = new Array(1).fill("injected script executed first"); + + SimpleTest.isDeeply( + JSON.parse(injectedScriptEval.result), + expectedResult, + "The injected script has been executed on the 'stopped' page reload" + ); + + // Reload again with no options. + + info("Reload the tab again without any reload options"); + const waitForNoInjectedScriptReload = waitForNextTabNavigated(commands); + await commands.inspectedWindowCommand.reload(fakeExtCallerInfo, {}); + await waitForNoInjectedScriptReload; + + const noInjectedScriptEval = await commands.scriptCommand.execute( + `(${collectEvalResults})()` + ); + + // The page should have stopped during the reload and no injected script should + // have been executed during this second reload (or it would mean that the previous + // customized reload was still pending and has wrongly affected the second reload) + const newExpectedResult = new Array(1).fill("injected script NOT executed"); + + SimpleTest.isDeeply( + JSON.parse(noInjectedScriptEval.result), + newExpectedResult, + "No injectedScript should have been evaluated during the second reload" + ); + + await teardown({ commands, extension }); +}); + +// TODO: check eval with $0 binding once implemented (Bug 1300590) diff --git a/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js new file mode 100644 index 0000000000..3b32bb0aaa --- /dev/null +++ b/devtools/shared/commands/inspected-window/tests/browser_webextension_inspected_window_access.js @@ -0,0 +1,315 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function run_inspectedWindow_eval({ tab, codeToEval, extension }) { + const fakeExtCallerInfo = { + url: `moz-extension://${extension.uuid}/another/fake-caller-script.js`, + lineNumber: 1, + addonId: extension.id, + }; + const commands = await CommandsFactory.forTab(tab, { isWebExtension: true }); + await commands.targetCommand.startListening(); + const result = await commands.inspectedWindowCommand.eval( + fakeExtCallerInfo, + codeToEval, + {} + ); + await commands.destroy(); + return result; +} + +async function openAboutBlankTabWithExtensionOrigin(extension) { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `moz-extension://${extension.uuid}/manifest.json` + ); + const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await ContentTask.spawn(tab.linkedBrowser, null, () => { + // about:blank inherits the principal when opened from content. + content.wrappedJSObject.location.assign("about:blank"); + }); + await loaded; + // Sanity checks: + is(tab.linkedBrowser.currentURI.spec, "about:blank", "expected tab"); + is( + tab.linkedBrowser.contentPrincipal.originNoSuffix, + `moz-extension://${extension.uuid}`, + "about:blank should be at the extension origin" + ); + return tab; +} + +async function checkEvalResult({ + extension, + description, + url, + createTab = () => BrowserTestUtils.openNewForegroundTab(gBrowser, url), + expectedResult, +}) { + const tab = await createTab(); + is(tab.linkedBrowser.currentURI.spec, url, "Sanity check: tab URL"); + const result = await run_inspectedWindow_eval({ + tab, + codeToEval: "'code executed at ' + location.href", + extension, + }); + BrowserTestUtils.removeTab(tab); + SimpleTest.isDeeply( + result, + expectedResult, + `eval result for devtools.inspectedWindow.eval at ${url} (${description})` + ); +} + +async function checkEvalAllowed({ extension, description, url, createTab }) { + info(`checkEvalAllowed: ${description} (at URL: ${url})`); + await checkEvalResult({ + extension, + description, + url, + createTab, + expectedResult: { value: `code executed at ${url}` }, + }); +} +async function checkEvalDenied({ extension, description, url, createTab }) { + info(`checkEvalDenied: ${description} (at URL: ${url})`); + await checkEvalResult({ + extension, + description, + url, + createTab, + expectedResult: { + exceptionInfo: { + isError: true, + code: "E_PROTOCOLERROR", + details: [ + "This extension is not allowed on the current inspected window origin", + ], + description: "Inspector protocol error: %s", + }, + }, + }); +} + +add_task(async function test_eval_at_http() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const httpUrl = "http://example.com/"; + + // When running with --use-http3-server, http:-URLs cannot be loaded. + try { + await fetch(httpUrl); + } catch { + info("Skipping test_eval_at_http because http:-URL cannot be loaded"); + return; + } + + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + + await checkEvalAllowed({ + extension, + description: "http:-URL", + url: httpUrl, + }); + await extension.unload(); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_eval_at_https() { + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + + const privilegedExtension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + }); + await privilegedExtension.startup(); + + await checkEvalAllowed({ + extension, + description: "https:-URL", + url: "https://example.com/", + }); + + await checkEvalDenied({ + extension, + description: "a restricted domain", + // Domain in extensions.webextensions.restrictedDomains by browser.toml. + url: "https://test2.example.com/", + }); + + await SpecialPowers.pushPrefEnv({ + set: [["extensions.quarantinedDomains.list", "example.com"]], + }); + + await checkEvalDenied({ + extension, + description: "a quarantined domain", + url: "https://example.com/", + }); + + await checkEvalAllowed({ + extension: privilegedExtension, + description: "a quarantined domain", + url: "https://example.com/", + }); + + await SpecialPowers.popPrefEnv(); + + await extension.unload(); + await privilegedExtension.unload(); +}); + +add_task(async function test_eval_at_sandboxed_page() { + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + + await checkEvalAllowed({ + extension, + description: "page with CSP sandbox", + url: "https://example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x", + }); + await checkEvalDenied({ + extension, + description: "restricted domain with CSP sandbox", + url: "https://test2.example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x", + }); + + await extension.unload(); +}); + +add_task(async function test_eval_at_own_extension_origin_allowed() { + const extension = ExtensionTestUtils.loadExtension({ + background() { + // eslint-disable-next-line no-undef + browser.test.sendMessage( + "blob_url", + URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }])) + ); + }, + files: { + "mozext.html": `<!DOCTYPE html>moz-extension: here`, + }, + }); + await extension.startup(); + const blobUrl = await extension.awaitMessage("blob_url"); + + await checkEvalAllowed({ + extension, + description: "moz-extension:-URL from own extension", + url: `moz-extension://${extension.uuid}/mozext.html`, + }); + await checkEvalAllowed({ + extension, + description: "blob:-URL from own extension", + url: blobUrl, + }); + await checkEvalAllowed({ + extension, + description: "about:blank with origin from own extension", + url: "about:blank", + createTab: () => openAboutBlankTabWithExtensionOrigin(extension), + }); + + await extension.unload(); +}); + +add_task(async function test_eval_at_other_extension_denied() { + // The extension for which we simulate devtools_page, chosen as caller of + // devtools.inspectedWindow.eval API calls. + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + + // The other extension, that |extension| should not be able to access: + const otherExt = ExtensionTestUtils.loadExtension({ + background() { + // eslint-disable-next-line no-undef + browser.test.sendMessage( + "blob_url", + URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }])) + ); + }, + files: { + "mozext.html": `<!DOCTYPE html>moz-extension: here`, + }, + }); + await otherExt.startup(); + const otherExtBlobUrl = await otherExt.awaitMessage("blob_url"); + + await checkEvalDenied({ + extension, + description: "moz-extension:-URL from another extension", + url: `moz-extension://${otherExt.uuid}/mozext.html`, + }); + await checkEvalDenied({ + extension, + description: "blob:-URL from another extension", + url: otherExtBlobUrl, + }); + await checkEvalDenied({ + extension, + description: "about:blank with origin from another extension", + url: "about:blank", + createTab: () => openAboutBlankTabWithExtensionOrigin(otherExt), + }); + + await otherExt.unload(); + await extension.unload(); +}); + +add_task(async function test_eval_at_about() { + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + await checkEvalAllowed({ + extension, + description: "about:blank (null principal)", + url: "about:blank", + }); + await checkEvalDenied({ + extension, + description: "about:addons (system principal)", + url: "about:addons", + }); + await checkEvalDenied({ + extension, + description: "about:robots (about page)", + url: "about:robots", + }); + await extension.unload(); +}); + +add_task(async function test_eval_at_file() { + // FYI: There is also an equivalent test case with a full end-to-end test at: + // browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js + + const extension = ExtensionTestUtils.loadExtension({}); + await extension.startup(); + + // A dummy file URL that can be loaded in a tab. + const fileUrl = + "file://" + + getTestFilePath("browser_webextension_inspected_window_access.js"); + + // checkEvalAllowed test helper cannot be used, because the file:-URL may + // redirect elsewhere, so the comparison with the full URL fails. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, fileUrl); + const result = await run_inspectedWindow_eval({ + tab, + codeToEval: "'code executed at ' + location.protocol", + extension, + }); + BrowserTestUtils.removeTab(tab); + SimpleTest.isDeeply( + result, + { value: "code executed at file:" }, + `eval result for devtools.inspectedWindow.eval at ${fileUrl}` + ); + + await extension.unload(); +}); diff --git a/devtools/shared/commands/inspected-window/tests/head.js b/devtools/shared/commands/inspected-window/tests/head.js new file mode 100644 index 0000000000..ce65b3d827 --- /dev/null +++ b/devtools/shared/commands/inspected-window/tests/head.js @@ -0,0 +1,12 @@ +/* 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"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); diff --git a/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs new file mode 100644 index 0000000000..2ec17a9222 --- /dev/null +++ b/devtools/shared/commands/inspected-window/tests/inspectedwindow-reload-target.sjs @@ -0,0 +1,89 @@ +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + const params = new URLSearchParams(request.queryString); + + switch (params.get("test")) { + case "cache": + handleCacheTestRequest(request, response); + break; + + case "user-agent": + handleUserAgentTestRequest(request, response); + break; + + case "injected-script": + handleInjectedScriptTestRequest(request, response, params); + break; + } +} + +function handleCacheTestRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + + if (request.hasHeader("pragma") && request.hasHeader("cache-control")) { + response.write( + `${request.getHeader("pragma")}:${request.getHeader("cache-control")}` + ); + } else { + response.write("empty cache headers"); + } +} + +function handleUserAgentTestRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + + if (request.hasHeader("user-agent")) { + response.write(request.getHeader("user-agent")); + } else { + response.write("no user agent header"); + } +} + +function handleInjectedScriptTestRequest(request, response, params) { + response.setHeader("Content-Type", "text/html; charset=UTF-8", false); + + const frames = parseInt(params.get("frames"), 10); + let content = ""; + + if (frames > 0) { + // Output an iframe in seamless mode, so that there is an higher chance that in case + // of test failures we get a screenshot where the nested iframes are all visible. + content = `<iframe seamless src="?test=injected-script&frames=${ + frames - 1 + }"></iframe>`; + } else { + // Output an about:srcdoc frame to be sure that inspectedWindow.eval is able to + // evaluate js code into it. + const srcdoc = ` + <pre>injected script NOT executed</pre> + <script>window.pageScriptExecutedFirst = true</script> + `; + content = `<iframe style="height: 30px;" srcdoc="${srcdoc}"></iframe>`; + } + + if (params.get("stop") == "windowStop") { + content = "<script>window.stop();</script>" + content; + } + + response.write(`<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <style> + iframe { width: 100%; height: ${frames * 150}px; } + </style> + </head> + <body> + <h1>IFRAME ${frames}</h1> + <pre>injected script NOT executed</pre> + <script> + window.pageScriptExecutedFirst = true; + </script> + ${content} + </body> + </html> + `); +} |