| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/containers/to_vector.h" |
| #include "base/i18n/icubridge/default_icu_locale.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/path_service.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/scoped_mock_time_message_loop_task_runner.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/test/test_future.h" |
| #include "base/test/values_test_util.h" |
| #include "base/values.h" |
| #include "build/chromeos_buildflags.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "ui/base/page_transition_types.h" |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_content_manager.h" |
| #endif |
| #include "chrome/browser/extensions/api/debugger/debugger_api.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_management_test_util.h" |
| #include "chrome/browser/extensions/extension_with_management_policy_apitest.h" |
| #include "chrome/browser/extensions/profile_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_destroyer.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profile_test_util.h" |
| #include "chrome/browser/pwc/privileged_web_contents.h" |
| #include "chrome/browser/pwc/pwc_component_policy.h" |
| #include "chrome/browser/tab_list/tab_list_interface.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/browser_closed_waiter.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/infobars/content/content_infobar_manager.h" |
| #include "components/infobars/core/infobar.h" |
| #include "components/infobars/core/infobar_delegate.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_interstitials/content/security_interstitial_controller_client.h" |
| #include "components/security_interstitials/content/security_interstitial_page.h" |
| #include "components/security_interstitials/content/security_interstitial_tab_helper.h" |
| #include "components/security_interstitials/content/settings_page_helper.h" |
| #include "components/security_interstitials/core/metrics_helper.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "content/public/browser/devtools_agent_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/mock_navigation_handle.h" |
| #include "content/public/test/no_renderer_crashes_assertion.h" |
| #include "extensions/browser/api_test_utils.h" |
| #include "extensions/browser/extension_function.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/permissions/permissions_updater.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/switches.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/base/filename_util.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "pdf/buildflags.h" |
| #include "services/network/public/mojom/cookie_manager.mojom.h" |
| #include "ui/base/base_window.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "components/messages/android/message_enums.h" |
| #include "components/messages/android/message_wrapper.h" |
| #include "components/messages/android/mock_message_dispatcher_bridge.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #else |
| #include "chrome/browser/extensions/api/debugger/extension_dev_tools_infobar_delegate.h" |
| #include "chrome/browser/infobars/infobar_features.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/with_feature_override.h" |
| #include "chrome/browser/pdf/pdf_extension_test_util.h" |
| #include "chrome/browser/pdf/test_mime_handler_stream_manager.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "pdf/pdf_features.h" |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| namespace extensions { |
| |
| namespace { |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Gets all URLs from the list of targets, with the ports removed. |
| std::vector<std::string> GetTargetUrlsWithoutPorts( |
| const base::ListValue& targets) { |
| return base::ToVector(targets, [](const base::Value& value) { |
| GURL::Replacements remove_port; |
| remove_port.ClearPort(); |
| const std::string* url = value.GetDict().FindString("url"); |
| return url ? GURL(*url).ReplaceComponents(remove_port).spec() |
| : "<missing field>"; |
| }); |
| } |
| |
| // Gets all targets as a list from the context. |
| // This method also filters out targets with "chrome://" URLs, such as the |
| // internal WebUI toolbar, to ensure tests are robust against the presence of |
| // these internal targets. |
| base::ListValue RunGetTargets( |
| content::BrowserContext* context, |
| api_test_utils::FunctionMode mode = api_test_utils::FunctionMode::kNone) { |
| auto get_targets_function = |
| base::MakeRefCounted<DebuggerGetTargetsFunction>(); |
| std::optional<base::Value> value = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| get_targets_function.get(), "[]", context, mode); |
| EXPECT_THAT( |
| value, testing::Optional(testing::Property(&base::Value::is_list, true))); |
| base::ListValue targets = std::move(*value).TakeList(); |
| targets.EraseIf([](const base::Value& target) { |
| const std::string* url = target.GetDict().FindString("url"); |
| return url && GURL(*url).SchemeIs(content::kChromeUIScheme); |
| }); |
| return targets; |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| } // namespace |
| |
| using testing::ElementsAre; |
| using testing::Eq; |
| |
| class DebuggerApiTest : public ExtensionApiTest { |
| protected: |
| DebuggerApiTest() { |
| feature_list_.InitAndDisableFeature( |
| extensions_features::kDisableExtensionsOnChromeUrlsSwitch); |
| } |
| ~DebuggerApiTest() override = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override; |
| void SetUpOnMainThread() override; |
| |
| // Run the attach function. If |expected_error| is not empty, then the |
| // function should fail with the error. Otherwise, the function is expected |
| // to succeed. If `ignore_navigation_errors` then navigation errors will be |
| // ignored. |
| testing::AssertionResult RunAttachFunction( |
| const GURL& url, |
| const std::string& expected_error, |
| bool ignore_navigation_errors = false); |
| testing::AssertionResult RunAttachFunction( |
| const content::WebContents* web_contents, |
| const std::string& expected_error); |
| |
| const Extension* extension() const { return extension_.get(); } |
| base::CommandLine* command_line() const { return command_line_; } |
| |
| void AdvanceClock(base::TimeDelta time) { clock_.Advance(time); } |
| |
| private: |
| testing::AssertionResult RunAttachFunctionOnTarget( |
| const std::string& debuggee_target, const std::string& expected_error); |
| |
| // The command-line for the test process, preserved in order to modify |
| // mid-test. |
| raw_ptr<base::CommandLine> command_line_; |
| |
| // A basic extension with the debugger permission. |
| scoped_refptr<const Extension> extension_; |
| |
| // A temporary directory in which to create and load from the |
| // |extension_|. |
| TestExtensionDir test_extension_dir_; |
| base::SimpleTestTickClock clock_; |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| void DebuggerApiTest::SetUpCommandLine(base::CommandLine* command_line) { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| // We need to hold onto |command_line| in order to modify it during the test. |
| command_line_ = command_line; |
| } |
| |
| class DebuggerFileAccessApiTest : public DebuggerApiTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| DebuggerApiTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(::switches::kAllowFileAccessFromFiles); |
| } |
| }; |
| |
| void DebuggerApiTest::SetUpOnMainThread() { |
| ExtensionApiTest::SetUpOnMainThread(); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| test_extension_dir_.WriteManifest( |
| R"({ |
| "name": "debugger", |
| "version": "0.1", |
| "manifest_version": 2, |
| "permissions": ["debugger"] |
| })"); |
| test_extension_dir_.WriteFile(FILE_PATH_LITERAL("test_file.html"), |
| "<html>Hello world!</html>"); |
| extension_ = LoadExtension(test_extension_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension_); |
| } |
| |
| testing::AssertionResult DebuggerApiTest::RunAttachFunction( |
| const GURL& url, |
| const std::string& expected_error, |
| bool ignore_navigation_errors) { |
| auto* web_contents = GetActiveWebContents(); |
| bool navigation_result = NavigateToURL(web_contents, url); |
| // Most navigations should succeed, but some are allowed to fail. |
| if (!ignore_navigation_errors) { |
| EXPECT_TRUE(navigation_result); |
| } |
| return RunAttachFunction(web_contents, expected_error); |
| } |
| |
| testing::AssertionResult DebuggerApiTest::RunAttachFunction( |
| const content::WebContents* web_contents, |
| const std::string& expected_error) { |
| // Attach by tabId. |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| std::string debugee_by_tab = base::StringPrintf("{\"tabId\": %d}", tab_id); |
| testing::AssertionResult result = |
| RunAttachFunctionOnTarget(debugee_by_tab, expected_error); |
| if (!result) { |
| return result; |
| } |
| |
| // Attach by targetId. |
| scoped_refptr<DebuggerGetTargetsFunction> get_targets_function = |
| new DebuggerGetTargetsFunction(); |
| std::optional<base::Value> value( |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| get_targets_function.get(), "[]", profile())); |
| EXPECT_TRUE(value->is_list()); |
| |
| std::string debugger_target_id; |
| for (const base::Value& target_value : value->GetList()) { |
| EXPECT_TRUE(target_value.is_dict()); |
| std::optional<int> id = target_value.GetDict().FindInt("tabId"); |
| if (id == tab_id) { |
| const std::string* id_str = target_value.GetDict().FindString("id"); |
| EXPECT_TRUE(id_str); |
| debugger_target_id = *id_str; |
| break; |
| } |
| } |
| EXPECT_TRUE(!debugger_target_id.empty()); |
| |
| std::string debugee_by_target_id = |
| base::StringPrintf("{\"targetId\": \"%s\"}", debugger_target_id.c_str()); |
| return RunAttachFunctionOnTarget(debugee_by_target_id, expected_error); |
| } |
| |
| testing::AssertionResult DebuggerApiTest::RunAttachFunctionOnTarget( |
| const std::string& debuggee_target, const std::string& expected_error) { |
| scoped_refptr<DebuggerAttachFunction> attach_function = |
| new DebuggerAttachFunction(); |
| attach_function->set_extension(extension_.get()); |
| |
| std::string actual_error; |
| if (!api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[%s, \"1.1\"]", debuggee_target.c_str()), |
| profile())) { |
| actual_error = attach_function->GetError(); |
| } else { |
| // Clean up and detach. |
| scoped_refptr<DebuggerDetachFunction> detach_function = |
| new DebuggerDetachFunction(); |
| detach_function->set_extension(extension_.get()); |
| if (!api_test_utils::RunFunction( |
| detach_function.get(), |
| base::StringPrintf("[%s]", debuggee_target.c_str()), profile())) { |
| return testing::AssertionFailure() << "Could not detach from " |
| << debuggee_target << " : " << detach_function->GetError(); |
| } |
| } |
| |
| if (expected_error.empty() && !actual_error.empty()) { |
| return testing::AssertionFailure() << "Could not attach to " |
| << debuggee_target << " : " << actual_error; |
| } else if (actual_error != expected_error) { |
| return testing::AssertionFailure() << "Did not get correct error upon " |
| << "attach to " << debuggee_target << " : " |
| << "expected: " << expected_error << ", found: " << actual_error; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| using PwcDebuggerApiTest = DebuggerApiTest; |
| |
| // An extension with the "debugger" permission cannot attach to, or even |
| // enumerate, a privileged WebContents (see //chrome's PrivilegedWebContents): |
| // it is invisible to extensions. |
| IN_PROC_BROWSER_TEST_F(PwcDebuggerApiTest, |
| CannotAttachToPrivilegedWebContents) { |
| std::unique_ptr<pwc::PrivilegedWebContents> privileged = |
| pwc::PrivilegedWebContents::Create( |
| pwc::PrivilegedComponent::kTestComponent, profile(), |
| std::make_unique<pwc::FixedPwcPolicyDelegate>( |
| std::vector<url::Origin>{}, std::vector<url::Origin>{})); |
| const std::string target_id = |
| content::DevToolsAgentHost::GetOrCreateFor(privileged->web_contents()) |
| ->GetId(); |
| |
| // chrome.debugger.getTargets does not list the privileged WebContents. |
| scoped_refptr<DebuggerGetTargetsFunction> get_targets = |
| base::MakeRefCounted<DebuggerGetTargetsFunction>(); |
| std::optional<base::Value> targets = |
| api_test_utils::RunFunctionAndReturnSingleResult(get_targets.get(), "[]", |
| profile()); |
| ASSERT_TRUE(targets && targets->is_list()); |
| for (const base::Value& target : targets->GetList()) { |
| const std::string* id = target.GetDict().FindString("id"); |
| EXPECT_FALSE(id && *id == target_id) |
| << "A privileged WebContents must not be listed by getTargets"; |
| } |
| |
| // Attaching by the privileged WebContents' target id fails. |
| scoped_refptr<DebuggerAttachFunction> attach = |
| base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach->set_extension(extension()); |
| EXPECT_FALSE(api_test_utils::RunFunction( |
| attach.get(), |
| base::StringPrintf(R"([{"targetId": "%s"}, "1.1"])", target_id.c_str()), |
| profile())); |
| EXPECT_EQ(manifest_errors::kCannotAccessPage, attach->GetError()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| DebuggerNotAllowedOnOtherExtensionPages) { |
| // Load another arbitrary extension with an associated resource (popup.html). |
| base::FilePath path; |
| ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path)); |
| path = path.AppendASCII("extensions").AppendASCII("simple_with_popup"); |
| const Extension* another_extension = LoadExtension(path); |
| ASSERT_TRUE(another_extension); |
| |
| GURL other_ext_url = another_extension->GetResourceURL("popup.html"); |
| |
| // This extension should not be able to access another extension. |
| EXPECT_TRUE(RunAttachFunction(other_ext_url, |
| manifest_errors::kCannotAccessExtensionUrl)); |
| |
| // This extension *should* be able to debug itself. |
| EXPECT_TRUE(RunAttachFunction(extension()->GetResourceURL("test_file.html"), |
| std::string())); |
| |
| // Append extensions on chrome urls switch. The extension should now be able |
| // to debug any extension. |
| command_line()->AppendSwitch(switches::kExtensionsOnChromeURLs); |
| EXPECT_TRUE(RunAttachFunction(other_ext_url, std::string())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerFileAccessApiTest, |
| DebuggerAllowedOnFileUrlsWithFileAccess) { |
| base::FilePath worker_path = |
| test_data_dir_.AppendASCII("debugger_file_access") |
| .AppendASCII("worker.html"); |
| GURL worker_url = net::FilePathToFileURL(worker_path); |
| std::string custom_arg = worker_url.spec() + "|enabled"; |
| EXPECT_TRUE(RunExtensionTest("debugger_file_access", |
| {.custom_arg = custom_arg.c_str()}, |
| {.allow_file_access = true})) |
| << message_; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/441339825): Fails on desktop Android with an error about |
| // access to localhost. |
| IN_PROC_BROWSER_TEST_F(DebuggerFileAccessApiTest, |
| DebuggerNotAllowedOnFileUrlsWithoutAccess) { |
| base::FilePath worker_path = |
| test_data_dir_.AppendASCII("debugger_file_access") |
| .AppendASCII("worker.html"); |
| GURL worker_url = net::FilePathToFileURL(worker_path); |
| std::string custom_arg = worker_url.spec() + "|disabled"; |
| |
| EXPECT_TRUE(RunExtensionTest("debugger_file_access", |
| {.custom_arg = custom_arg.c_str()})) |
| << message_; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| BrowserTargetAllowedForUnpackedPerfettoUIWithFlag) { |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| ::switches::kAllowUnpackedPerfettoExtension); |
| |
| scoped_refptr<const Extension> unpacked_perfetto = |
| ExtensionBuilder("Perfetto UI") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kUnpacked) |
| .AddAPIPermission("debugger") |
| .Build(); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(unpacked_perfetto.get()); |
| |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile())) |
| << attach_function->GetError(); |
| |
| // Clean up and detach. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(unpacked_perfetto.get()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), R"([{"targetId": "browser"}])", profile())); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| BrowserTargetNotAllowedForUnpackedPerfettoUI) { |
| scoped_refptr<const Extension> unpacked_perfetto = |
| ExtensionBuilder("Perfetto UI") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kUnpacked) |
| .AddAPIPermission("debugger") |
| .Build(); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(unpacked_perfetto.get()); |
| |
| std::string actual_error = api_test_utils::RunFunctionAndReturnError( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile()); |
| |
| EXPECT_EQ("No target with given id browser.", actual_error); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| BrowserTargetAllowedForComponentPerfettoUI) { |
| scoped_refptr<const Extension> component_perfetto = |
| ExtensionBuilder("Perfetto UI") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kComponent) |
| .AddAPIPermission("debugger") |
| .Build(); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(component_perfetto.get()); |
| |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile())) |
| << attach_function->GetError(); |
| |
| // Now, try to attach to a WebUI page via Target.attachToTarget through the |
| // browser target (which acts as a root session). This should be blocked by |
| // the child session's delegated MayAttachToRenderFrameHost check. |
| |
| // 1. Open a WebUI tab. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL("chrome://version"))); |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| |
| // 2. Find the targetId for the WebUI tab. |
| scoped_refptr<DebuggerGetTargetsFunction> get_targets = |
| new DebuggerGetTargetsFunction(); |
| std::optional<base::Value> targets_value( |
| api_test_utils::RunFunctionAndReturnSingleResult(get_targets.get(), "[]", |
| profile())); |
| ASSERT_TRUE(targets_value->is_list()); |
| |
| std::string webui_target_id; |
| for (const base::Value& target_value : targets_value->GetList()) { |
| std::optional<int> id = target_value.GetDict().FindInt("tabId"); |
| if (id == tab_id) { |
| const std::string* id_str = target_value.GetDict().FindString("id"); |
| ASSERT_TRUE(id_str); |
| webui_target_id = *id_str; |
| break; |
| } |
| } |
| ASSERT_FALSE(webui_target_id.empty()); |
| |
| // 3. Send Target.attachToTarget. |
| auto send_command = base::MakeRefCounted<DebuggerSendCommandFunction>(); |
| send_command->set_extension(component_perfetto.get()); |
| |
| std::string command_args = base::StringPrintf( |
| R"([{"targetId": "browser"}, "Target.attachToTarget", )" |
| R"({"targetId": "%s"}])", |
| webui_target_id.c_str()); |
| |
| // Run the command and expect it to fail (it will return an error response). |
| std::string attach_error = api_test_utils::RunFunctionAndReturnError( |
| send_command.get(), command_args, profile()); |
| |
| // The attach should fail with an error because the delegated |
| // MayAttachToRenderFrameHost check will block attaching to the WebUI frame. |
| EXPECT_THAT(attach_error, testing::HasSubstr("Not allowed")); |
| |
| // Clean up and detach. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(component_perfetto.get()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), R"([{"targetId": "browser"}])", profile())); |
| } |
| |
| class TestInterstitialPage |
| : public security_interstitials::SecurityInterstitialPage { |
| public: |
| TestInterstitialPage(content::WebContents* web_contents, |
| const GURL& request_url) |
| : SecurityInterstitialPage( |
| web_contents, |
| request_url, |
| std::make_unique< |
| security_interstitials::SecurityInterstitialControllerClient>( |
| web_contents, |
| CreateTestMetricsHelper(web_contents), |
| nullptr, |
| std::string(base::i18n::GetDefaultIcuLocale().tag_string()), |
| GURL(), |
| /* settings_page_helper*/ nullptr)) {} |
| |
| ~TestInterstitialPage() override = default; |
| void OnInterstitialClosing() override {} |
| |
| protected: |
| void PopulateInterstitialStrings(base::DictValue& load_time_data) override {} |
| |
| std::unique_ptr<security_interstitials::MetricsHelper> |
| CreateTestMetricsHelper(content::WebContents* web_contents) { |
| security_interstitials::MetricsHelper::ReportDetails report_details; |
| report_details.metric_prefix = "test_blocking_page"; |
| return std::make_unique<security_interstitials::MetricsHelper>( |
| GURL(), report_details, nullptr); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| DebuggerNotAllowedOnRestrictedBlobUrls) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| // Use chrome://version because it is webui on Win/Mac/Linux and Android. |
| EXPECT_TRUE(content::NavigateToURL(web_contents, GURL("chrome://version"))); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| ASSERT_TRUE(content::ExecJs(web_contents, R"( |
| var blob = new Blob([JSON.stringify({foo: 'bar'})], { |
| type: "application/json", |
| }); |
| var burl = URL.createObjectURL(blob, 'application/json'); |
| window.open(burl); |
| )")); |
| content::WebContents* blob_web_contents = GetActiveWebContents(); |
| EXPECT_NE(blob_web_contents, web_contents); |
| EXPECT_TRUE(content::WaitForLoadStop(blob_web_contents)); |
| EXPECT_EQ("{\"foo\":\"bar\"}", |
| content::EvalJs(blob_web_contents, "document.body.innerText")); |
| EXPECT_TRUE( |
| RunAttachFunction(blob_web_contents, "Cannot access a chrome:// URL")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| DebuggerNotAllowedOnPolicyRestrictedBlobUrls) { |
| GURL url(embedded_test_server()->GetURL("a.com", "/simple.html")); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(web_contents, url)); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| ASSERT_TRUE(content::ExecJs(web_contents, R"( |
| var blob = new Blob([JSON.stringify({foo: 'bar'})], { |
| type: "application/json", |
| }); |
| window.open(URL.createObjectURL(blob, 'application/json')); |
| )")); |
| content::WebContents* blob_web_contents = GetActiveWebContents(); |
| EXPECT_NE(blob_web_contents, web_contents); |
| EXPECT_TRUE(content::WaitForLoadStop(blob_web_contents)); |
| EXPECT_EQ("{\"foo\":\"bar\"}", |
| content::EvalJs(blob_web_contents, "document.body.innerText")); |
| base::RunLoop run_loop; |
| URLPatternSet default_blocked_hosts; |
| default_blocked_hosts.AddPattern( |
| URLPattern(URLPattern::SCHEME_HTTP, "http://a.com/*")); |
| PermissionsData::SetDefaultPolicyHostRestrictions( |
| util::GetBrowserContextId(profile()), default_blocked_hosts, |
| URLPatternSet()); |
| EXPECT_TRUE(RunAttachFunction(blob_web_contents, |
| "Host access is restricted by policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| DebuggerNotAllowedOnSecurityInterstitials) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| std::unique_ptr<content::MockNavigationHandle> navigation_handle = |
| std::make_unique<content::MockNavigationHandle>( |
| GURL("https://google.com/"), web_contents->GetPrimaryMainFrame()); |
| navigation_handle->set_has_committed(true); |
| navigation_handle->set_is_same_document(false); |
| EXPECT_TRUE(RunAttachFunction(web_contents, "")); |
| |
| security_interstitials::SecurityInterstitialTabHelper::AssociateBlockingPage( |
| navigation_handle.get(), |
| std::make_unique<TestInterstitialPage>(web_contents, GURL())); |
| security_interstitials::SecurityInterstitialTabHelper::FromWebContents( |
| web_contents) |
| ->DidFinishNavigation(navigation_handle.get()); |
| |
| EXPECT_TRUE(RunAttachFunction(web_contents, "Cannot attach to this target.")); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android uses messages for warnings instead of infobars. |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, Messages) { |
| int tab_id = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| scoped_refptr<DebuggerAttachFunction> attach_function; |
| scoped_refptr<DebuggerDetachFunction> detach_function; |
| |
| messages::MockMessageDispatcherBridge mock_bridge; |
| mock_bridge.SetMessagesEnabledForEmbedder(true); |
| messages::MessageDispatcherBridge::SetInstanceForTesting(&mock_bridge); |
| |
| // Set up mocks to capture the message and handle cleanup. |
| messages::MessageWrapper* captured_message = nullptr; |
| EXPECT_CALL(mock_bridge, |
| EnqueueWindowScopedMessage(testing::_, testing::_, |
| messages::MessagePriority::kUrgent)) |
| .WillRepeatedly([&captured_message](messages::MessageWrapper* message, |
| ui::WindowAndroid* window, |
| messages::MessagePriority priority) { |
| captured_message = message; |
| message->SetMessageEnqueued(window->GetJavaObject()); |
| return true; |
| }); |
| EXPECT_CALL( |
| mock_bridge, |
| DismissMessage(testing::_, messages::DismissReason::DISMISSED_BY_FEATURE)) |
| .WillRepeatedly([&captured_message]( |
| messages::MessageWrapper* message, |
| messages::DismissReason dismiss_reason) { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| message->HandleDismissCallback(env, static_cast<int>(dismiss_reason)); |
| captured_message = nullptr; |
| }); |
| |
| // Attaching should create a message. |
| attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| ASSERT_NE(nullptr, captured_message); |
| EXPECT_TRUE(captured_message->is_in_queue()); |
| |
| // Detaching removes the message. |
| detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| EXPECT_FALSE(captured_message); |
| |
| // Attach again; should create another message. |
| attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_TRUE(captured_message->is_in_queue()); |
| |
| // Simulating what happens when the user clicks the cancel button. The |
| // extension is detached. |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| captured_message->HandleActionClick(env); |
| |
| // The message is closed. |
| EXPECT_FALSE(captured_message); |
| |
| // Trying to detach again will fail because the extension is already detached. |
| detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_FALSE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| |
| messages::MessageDispatcherBridge::SetInstanceForTesting(nullptr); |
| } |
| |
| #else // BUILDFLAG(IS_ANDROID) |
| |
| // Win/Mac/Linux/Chrome OS use infobars for warnings. |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, InfoBar) { |
| int tab_id = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| scoped_refptr<DebuggerAttachFunction> attach_function; |
| scoped_refptr<DebuggerDetachFunction> detach_function; |
| |
| // Create a second browser with two tabs. |
| BrowserWindowInterface* browser2 = |
| CreateBrowserWindowWithType(BrowserWindowInterface::TYPE_NORMAL); |
| TabListInterface* tab_list2 = TabListInterface::From(browser2); |
| // Platforms like Win/Mac/Linux create browsers with no tabs, whereas Android |
| // creates browsers with a single tab. |
| if (tab_list2->GetTabCount() == 0) { |
| tab_list2->OpenTab(GURL("about:blank"), /*index=*/-1); |
| } |
| tab_list2->OpenTab(GURL("about:blank"), /*index=*/-1); |
| ASSERT_EQ(2, tab_list2->GetTabCount()); |
| int tab_id2 = sessions::SessionTabHelper::IdForTab( |
| tab_list2->GetActiveTab()->GetContents()) |
| .id(); |
| |
| infobars::ContentInfoBarManager* manager1 = |
| infobars::ContentInfoBarManager::FromWebContents(GetActiveWebContents()); |
| infobars::ContentInfoBarManager* manager2 = |
| infobars::ContentInfoBarManager::FromWebContents( |
| tab_list2->GetTab(0)->GetContents()); |
| infobars::ContentInfoBarManager* manager3 = |
| infobars::ContentInfoBarManager::FromWebContents( |
| tab_list2->GetTab(1)->GetContents()); |
| |
| // The infobar framework shows a global infobar only in the |
| // active tab of each browser window, moving it as the active tab changes. |
| // The legacy GlobalConfirmInfoBar instead puts an infobar in every tab. Only |
| // `manager2` is a background tab (browser2's active tab is `manager3`), so |
| // it is the only expectation that differs between the two implementations. |
| const bool global_infobar_in_active_tab_only = infobars::IsInfoBarMigrated( |
| infobars::InfoBarDelegate::EXTENSION_DEV_TOOLS_INFOBAR_DELEGATE); |
| const size_t background_tab_infobars = |
| global_infobar_in_active_tab_only ? 0u : 1u; |
| // The infobar the user would click the close button on must be one that is |
| // actually showing. |
| infobars::ContentInfoBarManager* dismissal_manager = |
| global_infobar_in_active_tab_only ? manager3 : manager2; |
| |
| // Attaching to one tab should create infobars in both browsers. |
| attach_function = new DebuggerAttachFunction(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Attaching to another tab should not create more infobars. |
| attach_function = new DebuggerAttachFunction(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id2), profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Detaching from one of the tabs should not remove infobars. |
| detach_function = new DebuggerDetachFunction(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id2), |
| profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Detaching from the other tab also should not remove infobars, since even |
| // though there is no longer an extension attached, the infobar can only be |
| // dismissed by explicit user action. |
| detach_function = new DebuggerDetachFunction(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Attach again; should not create infobars. |
| attach_function = new DebuggerAttachFunction(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Remove the global infobar by simulating what happens when the user clicks |
| // the close button (see InfoBarView::ButtonPressed()). The |
| // InfoBarDismissed() call will remove the infobars everywhere except on |
| // |dismissal_manager| itself; the RemoveSelf() call removes that one. |
| dismissal_manager->infobars()[0]->delegate()->InfoBarDismissed(); |
| dismissal_manager->infobars()[0]->RemoveSelf(); |
| EXPECT_EQ(0u, manager1->infobars().size()); |
| EXPECT_EQ(0u, manager2->infobars().size()); |
| EXPECT_EQ(0u, manager3->infobars().size()); |
| detach_function = new DebuggerDetachFunction(); |
| detach_function->set_extension(extension()); |
| // Cannot detach again. |
| ASSERT_FALSE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| |
| // Attaching once again should create a new infobar. |
| attach_function = new DebuggerAttachFunction(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(background_tab_infobars, manager2->infobars().size()); |
| EXPECT_EQ(1u, manager3->infobars().size()); |
| |
| // Closing tab should not affect anything. Note that under the infobar |
| // framework this makes `manager2`'s tab active, so the global |
| // infobar moves there. |
| EXPECT_EQ(2, tab_list2->GetTabCount()); |
| tab_list2->CloseTab(tab_list2->GetTab(1)->GetHandle()); |
| EXPECT_EQ(1, tab_list2->GetTabCount()); |
| manager3 = nullptr; |
| dismissal_manager = nullptr; |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| EXPECT_EQ(1u, manager2->infobars().size()); |
| |
| // Closing browser should not affect anything. Use a waiter because browser |
| // close is async on some platforms (e.g. Android). |
| BrowserClosedWaiter waiter(browser2); |
| browser2->GetWindow()->Close(); |
| waiter.Wait(); |
| manager2 = nullptr; |
| browser2 = nullptr; |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| |
| // Detach should not affect anything. |
| detach_function = new DebuggerDetachFunction(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| EXPECT_EQ(1u, manager1->infobars().size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, InfoBarIsRemovedAfterFiveSeconds) { |
| int tab_id = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| infobars::ContentInfoBarManager* manager = |
| infobars::ContentInfoBarManager::FromWebContents(GetActiveWebContents()); |
| |
| // Attaching to the tab should create an infobar. |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Detaching from the tab should remove the infobar after 5 seconds. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| |
| // Even though the extension detached, the infobar should not detach |
| // immediately, and should remain visible for 5 seconds to ensure the user |
| // has an opportunity to see it. |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| EXPECT_EQ(1u, manager->infobars().size()); // Infobar is still shown. |
| |
| // Advance the clock by 5 seconds, and verify the infobar is removed. |
| AdvanceClock(ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| run_loop.Run(); |
| |
| EXPECT_EQ(0u, manager->infobars().size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| InfoBarIsNotRemovedWhenAnotherDebuggerAttached) { |
| const int tab_id1 = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| ASSERT_TRUE( |
| NavigateToURLInNewTab(embedded_test_server()->GetURL("/simple.html"))); |
| const int tab_id2 = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| |
| // Watch the active tab's manager: the infobar framework only |
| // shows a global infobar in the active tab of each browser window, while the |
| // legacy GlobalConfirmInfoBar shows one in every tab. The active tab has an |
| // infobar either way. |
| infobars::ContentInfoBarManager* manager = |
| infobars::ContentInfoBarManager::FromWebContents(GetActiveWebContents()); |
| |
| // Attaching to a tab should create an infobar. |
| { |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id1), profile())); |
| } |
| |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Attaching to a 2nd tab, to have another attached debugger. |
| { |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id2), profile())); |
| } |
| |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Detaching from the tab should not remove the infobar after 5 seconds, as |
| // another debugger is still attached. |
| { |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id1), |
| profile())); |
| } |
| |
| // Advance the clock by 5 seconds. |
| { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| AdvanceClock(ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| run_loop.Run(); |
| } |
| |
| // Verify inforbar not removed. |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Now detach the last debugger. |
| { |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id2), |
| profile())); |
| } |
| |
| // Advance the clock by 5 seconds, once again. |
| { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| AdvanceClock(ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| run_loop.Run(); |
| } |
| |
| // Verify inforbar removed. |
| EXPECT_EQ(0u, manager->infobars().size()); |
| } |
| |
| // Android does not support multiple profiles in Chrome. User switching is |
| // handled at the OS level. |
| class CrossProfileDebuggerApiTest : public DebuggerApiTest { |
| protected: |
| Profile* other_profile() { return other_profile_; } |
| Profile* otr_profile() { return otr_profile_; } |
| |
| std::unique_ptr<content::WebContents> CreateTabWithProfileAndNavigate( |
| Profile* profile, |
| const GURL& url) { |
| auto wc = content::WebContents::Create( |
| content::WebContents::CreateParams(profile)); |
| EXPECT_TRUE(content::NavigateToURL(wc.get(), url)); |
| return wc; |
| } |
| |
| private: |
| void SetUpOnMainThread() override { |
| #if BUILDFLAG(IS_CHROMEOS) |
| ash::ProfileHelper::SetAlwaysReturnPrimaryUserForTesting(true); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| DebuggerApiTest::SetUpOnMainThread(); |
| ProfileManager* const profile_manager = profile_util::GetProfileManager(); |
| |
| other_profile_ = &profiles::testing::CreateProfileSync( |
| profile_manager, profile_manager->GenerateNextProfileDirectoryPath()); |
| otr_profile_ = profile()->GetPrimaryOTRProfile(true); |
| } |
| |
| void TearDownOnMainThread() override { |
| ProfileDestroyer::DestroyOTRProfileWhenAppropriate(otr_profile_); |
| DebuggerApiTest::TearDownOnMainThread(); |
| } |
| |
| raw_ptr<Profile, DanglingUntriaged> other_profile_ = nullptr; |
| raw_ptr<Profile, DanglingUntriaged> otr_profile_ = nullptr; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(CrossProfileDebuggerApiTest, GetTargets) { |
| auto wc1 = CreateTabWithProfileAndNavigate( |
| other_profile(), |
| embedded_test_server()->GetURL("/simple.html?other_profile")); |
| auto wc2 = CreateTabWithProfileAndNavigate( |
| otr_profile(), |
| embedded_test_server()->GetURL("/simple.html?off_the_record")); |
| |
| { |
| base::ListValue targets = RunGetTargets(profile()); |
| EXPECT_THAT(targets, ElementsAre(base::test::DictionaryHasValue( |
| "url", base::Value("about:blank")))); |
| } |
| |
| { |
| const base::ListValue targets = |
| RunGetTargets(profile(), api_test_utils::FunctionMode::kIncognito); |
| std::vector<std::string> urls = GetTargetUrlsWithoutPorts(targets); |
| EXPECT_THAT(urls, testing::UnorderedElementsAre( |
| "about:blank", |
| "http://127.0.0.1/simple.html?off_the_record")); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CrossProfileDebuggerApiTest, Attach) { |
| auto wc1 = CreateTabWithProfileAndNavigate( |
| other_profile(), |
| embedded_test_server()->GetURL("/simple.html?other_profile")); |
| std::string target_in_other_profile = base::StringPrintf( |
| "[{\"targetId\": \"%s\"}, \"1.1\"]", |
| content::DevToolsAgentHost::GetOrCreateFor(wc1.get())->GetId().c_str()); |
| |
| { |
| auto debugger_attach_function = |
| base::MakeRefCounted<DebuggerAttachFunction>(); |
| debugger_attach_function->set_extension(extension()); |
| EXPECT_FALSE(api_test_utils::RunFunction( |
| debugger_attach_function.get(), target_in_other_profile, profile())); |
| } |
| { |
| auto debugger_attach_function = |
| base::MakeRefCounted<DebuggerAttachFunction>(); |
| debugger_attach_function->set_extension(extension()); |
| EXPECT_FALSE(api_test_utils::RunFunction( |
| debugger_attach_function.get(), target_in_other_profile.c_str(), |
| profile(), api_test_utils::FunctionMode::kIncognito)); |
| } |
| |
| auto wc2 = CreateTabWithProfileAndNavigate( |
| otr_profile(), |
| embedded_test_server()->GetURL("/simple.html?off_the_record")); |
| std::string target_in_otr_profile = base::StringPrintf( |
| "[{\"targetId\": \"%s\"}, \"1.1\"]", |
| content::DevToolsAgentHost::GetOrCreateFor(wc2.get())->GetId().c_str()); |
| |
| { |
| auto debugger_attach_function = |
| base::MakeRefCounted<DebuggerAttachFunction>(); |
| debugger_attach_function->set_extension(extension()); |
| EXPECT_FALSE(api_test_utils::RunFunction(debugger_attach_function.get(), |
| target_in_otr_profile.c_str(), |
| profile())); |
| } |
| { |
| auto debugger_attach_function = |
| base::MakeRefCounted<DebuggerAttachFunction>(); |
| debugger_attach_function->set_extension(extension()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| debugger_attach_function.get(), target_in_otr_profile.c_str(), |
| profile(), api_test_utils::FunctionMode::kIncognito)); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, |
| InfoBarIsNotRemovedIfAttachAgainBeforeFiveSeconds) { |
| int tab_id = |
| sessions::SessionTabHelper::IdForTab(GetActiveWebContents()).id(); |
| infobars::ContentInfoBarManager* manager = |
| infobars::ContentInfoBarManager::FromWebContents(GetActiveWebContents()); |
| |
| // Attaching to the tab should create an infobar. |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Detaching from the tab and attaching it again before 5 seconds should not |
| // remove the infobar. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), base::StringPrintf("[{\"tabId\": %d}]", tab_id), |
| profile())); |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(extension()); |
| ASSERT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), |
| base::StringPrintf("[{\"tabId\": %d}, \"1.1\"]", tab_id), profile())); |
| // Verify that only one infobar is created. |
| EXPECT_EQ(1u, manager->infobars().size()); |
| |
| // Verify that infobar is not closed after 5 seconds. |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), |
| ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| AdvanceClock(ExtensionDevToolsInfoBarDelegate::kAutoCloseDelay); |
| run_loop.Run(); |
| |
| EXPECT_EQ(1u, manager->infobars().size()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // Tests that policy blocked hosts supersede the `debugger` |
| // permission. Regression test for crbug.com/40053634. |
| IN_PROC_BROWSER_TEST_F(DebuggerApiTest, TestDefaultPolicyBlockedHosts) { |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| GURL url("https://example.com/test"); |
| // The file does not exist so ignore navigation errors. |
| EXPECT_TRUE( |
| RunAttachFunction(url, std::string(), /*ignore_navigation_errors=*/true)); |
| URLPatternSet default_blocked_hosts; |
| default_blocked_hosts.AddPattern( |
| URLPattern(URLPattern::SCHEME_HTTPS, "https://example.com/*")); |
| PermissionsData::SetDefaultPolicyHostRestrictions( |
| util::GetBrowserContextId(profile()), default_blocked_hosts, |
| URLPatternSet()); |
| |
| EXPECT_TRUE(RunAttachFunction(url, "Host access is restricted by policy.", |
| /*ignore_navigation_errors=*/true)); |
| } |
| |
| class DebuggerExtensionApiTest : public ExtensionApiTest { |
| public: |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| } |
| }; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/441339825): Fails on desktop Android. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, Debugger) { |
| ASSERT_TRUE(RunExtensionTest("debugger")) << message_; |
| } |
| #endif |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, DebuggerMv3) { |
| ASSERT_TRUE(RunExtensionTest("debugger_mv3")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, ParentTargetPermissions) { |
| // Run test with file access disabled. |
| ASSERT_TRUE(RunExtensionTest("parent_target_permissions")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, ReloadAndResetHistory) { |
| // Run test with file access disabled. |
| ASSERT_TRUE(RunExtensionTest("debugger_reload_and_reset_history")) |
| << message_; |
| } |
| |
| // Tests that an extension is not allowed to inspect a worker through the |
| // inspectWorker debugger command. |
| // Regression test for https://crbug.com/40051715. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, |
| DebuggerNotAllowedToInvokeInspectWorker) { |
| GURL url(embedded_test_server()->GetURL( |
| "/extensions/api_test/debugger_inspect_worker/inspected_page.html")); |
| |
| EXPECT_TRUE(RunExtensionTest("debugger_inspect_worker", |
| {.custom_arg = url.spec().c_str()})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, AttachToEmptyUrls) { |
| ASSERT_TRUE(RunExtensionTest("debugger_attach_to_empty_urls")) << message_; |
| } |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| class DebuggerExtensionApiPdfTest : public base::test::WithFeatureOverride, |
| public DebuggerExtensionApiTest { |
| public: |
| DebuggerExtensionApiPdfTest() |
| : base::test::WithFeatureOverride(chrome_pdf::features::kPdfOopif) {} |
| }; |
| |
| // Test that the debuggers can attach to the PDF embedder frame. |
| IN_PROC_BROWSER_TEST_P(DebuggerExtensionApiPdfTest, AttachToPdf) { |
| ASSERT_TRUE(RunExtensionTest("debugger_attach_to_pdf")) << message_; |
| } |
| |
| // TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer |
| // launches. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(DebuggerExtensionApiPdfTest); |
| |
| class DebuggerExtensionApiOopifPdfTest : public DebuggerExtensionApiTest { |
| public: |
| DebuggerExtensionApiOopifPdfTest() { |
| feature_list_.InitAndEnableFeature(chrome_pdf::features::kPdfOopif); |
| } |
| |
| pdf::TestMimeHandlerStreamManager* GetTestMimeHandlerStreamManager() { |
| return factory_.GetTestMimeHandlerStreamManager(GetActiveWebContents()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| pdf::TestMimeHandlerStreamManagerFactory factory_; |
| }; |
| |
| // Test that the inner PDF frames, i.e. the PDF extension frame and the PDF |
| // content frame, aren't visible targets, while the PDF embedder frame is. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiOopifPdfTest, GetTargets) { |
| GURL pdf_url(embedded_test_server()->GetURL("/pdf/test.pdf")); |
| |
| // Load a full-page PDF. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), pdf_url)); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(GetTestMimeHandlerStreamManager()->WaitUntilPdfLoaded( |
| web_contents->GetPrimaryMainFrame())); |
| |
| // Get targets. |
| base::ListValue targets = RunGetTargets(profile()); |
| |
| // Verify that the inner PDF frames aren't targets in the list. Only the PDF |
| // embedder frame (the main frame) should be a target. |
| ASSERT_THAT(targets, testing::SizeIs(1)); |
| |
| // Verify that the target is the PDF embedder frame. |
| std::vector<std::string> urls = GetTargetUrlsWithoutPorts(targets); |
| ASSERT_THAT(urls, testing::SizeIs(1)); |
| EXPECT_EQ(urls[0], "http://127.0.0.1/pdf/test.pdf"); |
| } |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/441339825): Fails on desktop Android. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, AttachToBlob) { |
| ASSERT_TRUE(RunExtensionTest("debugger_attach_to_blob_urls")) << message_; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests that navigation to a forbidden URL is properly denied and |
| // does not cause a crash. |
| // This is a regression test for https://crbug.com/40055226. |
| // TODO(crbug.com/41483732): Re-enable this test. |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \ |
| BUILDFLAG(IS_ANDROID) |
| #define MAYBE_NavigateToForbiddenUrl DISABLED_NavigateToForbiddenUrl |
| #else |
| #define MAYBE_NavigateToForbiddenUrl NavigateToForbiddenUrl |
| #endif |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, MAYBE_NavigateToForbiddenUrl) { |
| content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes; |
| ASSERT_TRUE(RunExtensionTest("debugger_navigate_to_forbidden_url")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, NavigateToUntrustedWebUIUrl) { |
| ASSERT_TRUE(RunExtensionTest("debugger_navigate_to_untrusted_webui_url")) |
| << message_; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests that Target.createTarget to WebUI origins are blocked. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionApiTest, CreateTargetToUntrustedWebUI) { |
| ASSERT_TRUE(RunExtensionTest("debugger_create_target_to_untrusted_webui")) |
| << message_; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| class SitePerProcessDebuggerExtensionApiTest : public DebuggerExtensionApiTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| DebuggerExtensionApiTest::SetUpCommandLine(command_line); |
| content::IsolateAllSitesForTesting(command_line); |
| } |
| }; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/441339825): Fails on desktop Android. |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, Debugger) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", "/extensions/api_test/debugger/oopif.html")); |
| GURL iframe_url(embedded_test_server()->GetURL( |
| "b.com", "/extensions/api_test/debugger/oopif_frame.html")); |
| content::WebContents* tab = GetActiveWebContents(); |
| content::TestNavigationManager navigation_manager(tab, url); |
| content::TestNavigationManager navigation_manager_iframe(tab, iframe_url); |
| tab->GetController().LoadURL(url, content::Referrer(), |
| ui::PAGE_TRANSITION_LINK, std::string()); |
| ASSERT_TRUE(navigation_manager.WaitForNavigationFinished()); |
| ASSERT_TRUE(navigation_manager_iframe.WaitForNavigationFinished()); |
| EXPECT_TRUE(content::WaitForLoadStop(tab)); |
| |
| ASSERT_TRUE(RunExtensionTest("debugger", |
| {.custom_arg = "oopif.html;oopif_frame.html"})) |
| << message_; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, |
| NavigateSubframe) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", |
| "/extensions/api_test/debugger_navigate_subframe/inspected_page.html")); |
| ASSERT_TRUE(RunExtensionTest("debugger_navigate_subframe", |
| {.custom_arg = url.spec().c_str()})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, |
| AutoAttachPermissions) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", |
| "/extensions/api_test/debugger_auto_attach_permissions/page.html")); |
| ASSERT_TRUE(RunExtensionTest("debugger_auto_attach_permissions", |
| {.custom_arg = url.spec().c_str()})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, |
| AutoAttachFlatModePermissions) { |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", |
| "/extensions/api_test/debugger_auto_attach_flat_mode_permissions/" |
| "page.html")); |
| ASSERT_TRUE(RunExtensionTest("debugger_auto_attach_flat_mode_permissions", |
| {.custom_arg = url.spec().c_str()})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, |
| DebuggerCheckInnerUrl) { |
| ASSERT_TRUE(RunExtensionTest("debugger_check_inner_url")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SitePerProcessDebuggerExtensionApiTest, |
| OopifAutoAttachWebAccessibleResourcesBypass) { |
| TestExtensionDir victim_dir; |
| victim_dir.WriteManifest(R"({ |
| "name": "Victim", |
| "version": "1.0", |
| "manifest_version": 3 |
| })"); |
| victim_dir.WriteFile(FILE_PATH_LITERAL("restricted.html"), |
| "<html>Restricted</html>"); |
| const Extension* victim = LoadExtension(victim_dir.UnpackedPath()); |
| ASSERT_TRUE(victim); |
| |
| GURL url(embedded_test_server()->GetURL( |
| "a.com", |
| "/extensions/api_test/debugger_oopif_auto_attach_war_bypass/page.html")); |
| |
| std::string custom_arg = url.spec() + ";" + victim->id(); |
| ASSERT_TRUE(RunExtensionTest("debugger_oopif_auto_attach_war_bypass", |
| {.custom_arg = custom_arg.c_str()})) |
| << message_; |
| } |
| |
| // Test fixture for verifying chrome.debugger screenshot policy behavior when |
| // the kExtensionDebuggerStrictPolicyRestrictions feature flag is explicitly |
| // enabled (crbug.com/561948316). |
| class SitePerProcessDebuggerExtensionApiStrictPolicyEnabledTest |
| : public SitePerProcessDebuggerExtensionApiTest { |
| public: |
| SitePerProcessDebuggerExtensionApiStrictPolicyEnabledTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| extensions_features::kExtensionDebuggerStrictPolicyRestrictions); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| SitePerProcessDebuggerExtensionApiStrictPolicyEnabledTest, |
| AttachRejectedWhenScreenshotsDisabled) { |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true); |
| |
| ASSERT_TRUE(RunExtensionTest( |
| "debugger_disable_screenshots", |
| {.custom_arg = "Screenshot capture is restricted by policy."})) |
| << message_; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Target-level screenshot restrictions via Data Leak Prevention (DLP) are |
| // currently only supported on ChromeOS. |
| IN_PROC_BROWSER_TEST_F( |
| SitePerProcessDebuggerExtensionApiStrictPolicyEnabledTest, |
| AttachRejectedWhenScreenshotsRestrictedByDlp) { |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| policy::MockDlpContentManager mock_dlp_content_manager; |
| policy::ScopedDlpContentObserverForTesting scoped_dlp_content_observer( |
| &mock_dlp_content_manager); |
| EXPECT_CALL(mock_dlp_content_manager, |
| IsScreenshotApiRestricted(web_contents())) |
| .WillOnce(testing::Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest( |
| "debugger_disable_screenshots", |
| {.custom_arg = "Screenshot capture is restricted on this target."})) |
| << message_; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Tests that a trusted component extension is exempt from global screenshot |
| // restrictions and can attach to the browser target even when enterprise |
| // policy disables screenshots. |
| IN_PROC_BROWSER_TEST_F( |
| SitePerProcessDebuggerExtensionApiStrictPolicyEnabledTest, |
| BrowserTargetAllowedForTrustedExtensionWhenScreenshotsDisabled) { |
| profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true); |
| |
| scoped_refptr<const Extension> component_trusted_extension = |
| ExtensionBuilder("Some Trusted Extension") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kComponent) |
| .AddAPIPermission("debugger") |
| .Build(); |
| PermissionsUpdater(profile()).InitializePermissions( |
| component_trusted_extension.get()); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(component_trusted_extension.get()); |
| |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile())) |
| << attach_function->GetError(); |
| |
| // Clean up and detach. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(component_trusted_extension.get()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), R"([{"targetId": "browser"}])", profile())); |
| } |
| |
| class DebuggerExtensionManagementPolicyTestBase |
| : public ExtensionApiTestWithManagementPolicy { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTestWithManagementPolicy::SetUpCommandLine(command_line); |
| content::IsolateAllSitesForTesting(command_line); |
| } |
| }; |
| |
| // Test fixture for verifying chrome.debugger runtime_blocked_hosts policy |
| // behavior when the kExtensionDebuggerStrictPolicyRestrictions feature flag is |
| // explicitly enabled (crbug.com/561948316). |
| class DebuggerExtensionManagementPolicyTest |
| : public DebuggerExtensionManagementPolicyTestBase { |
| public: |
| DebuggerExtensionManagementPolicyTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| extensions_features::kExtensionDebuggerStrictPolicyRestrictions); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that attaching the debugger to a blocked host is rejected when |
| // runtime blocked hosts are configured globally via enterprise policy (*). |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionManagementPolicyTest, |
| AttachRejectedWithPolicyBlockedHostsGlobal_BlockedHost) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| // Set up runtime blocked hosts globally for all extensions. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://a.test"); |
| } |
| |
| // Attempting to attach to a blocked host fails with policy error. |
| ASSERT_TRUE( |
| RunExtensionTest("debugger_policy_blocked_hosts", |
| {.custom_arg = "Host access is restricted by policy."})) |
| << message_; |
| } |
| |
| // Tests that under the all-or-nothing model, attaching the debugger to an |
| // unblocked host is also rejected when runtime blocked hosts are configured |
| // globally via enterprise policy (*). |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyTest, |
| AttachRejectedWithPolicyBlockedHostsGlobal_UnblockedHost) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("b.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| // Set up runtime blocked hosts globally for all extensions (blocking a.test). |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://a.test"); |
| } |
| |
| // Attempting to attach to an unblocked host (b.test) also fails. |
| ASSERT_TRUE( |
| RunExtensionTest("debugger_policy_blocked_hosts", |
| {.custom_arg = "Host access is restricted by policy."})) |
| << message_; |
| } |
| |
| // Tests that attaching the debugger is rejected when runtime blocked hosts are |
| // configured specifically for this extension. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionManagementPolicyTest, |
| AttachRejectedWithPolicyBlockedHostsPerExtension) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| ExtensionId extension_id = crx_file::id_util::GenerateIdForPath( |
| test_data_dir_.AppendASCII("debugger_policy_blocked_hosts")); |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost(extension_id, "*://a.test"); |
| } |
| |
| ASSERT_TRUE( |
| RunExtensionTest("debugger_policy_blocked_hosts", |
| {.custom_arg = "Host access is restricted by policy."})) |
| << message_; |
| } |
| |
| // Tests that an extension can attach when it explicitly overrides the global |
| // blocked hosts policy with empty blocked hosts. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionManagementPolicyTest, |
| AttachAllowedWhenPolicyBlockedHostsOverridden) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| ExtensionId extension_id = crx_file::id_util::GenerateIdForPath( |
| test_data_dir_.AppendASCII("debugger_policy_blocked_hosts")); |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://a.test"); |
| pref.ClearPolicyBlockedHosts(extension_id); |
| } |
| |
| ASSERT_TRUE(RunExtensionTest("debugger_policy_blocked_hosts")) << message_; |
| } |
| |
| // Tests that when enterprise policy uses runtime_blocked_hosts and |
| // runtime_allowed_hosts to configure an allowlist of URLs, attaching the |
| // debugger is still rejected even on an allowlisted host. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionManagementPolicyTest, |
| AttachRejectedWithPolicyAllowlist) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| // Set up an allowlist: block all hosts, but allow a.test. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://*"); |
| pref.AddPolicyAllowedHost("*", "*://a.test"); |
| } |
| |
| // Under the all-or-nothing model, attaching is rejected even on an |
| // allowlisted host. |
| ASSERT_TRUE( |
| RunExtensionTest("debugger_policy_blocked_hosts", |
| {.custom_arg = "Host access is restricted by policy."})) |
| << message_; |
| } |
| |
| // Tests that an extension can attach normally when no runtime blocked hosts |
| // are configured by enterprise policy. |
| IN_PROC_BROWSER_TEST_F(DebuggerExtensionManagementPolicyTest, |
| AttachAllowedWithoutPolicyBlockedHosts) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| ASSERT_TRUE(RunExtensionTest("debugger_policy_blocked_hosts")) << message_; |
| } |
| |
| // Tests that a trusted component extension is exempt from the |
| // policy_blocked_hosts check and can attach to the browser target even when |
| // runtime blocked hosts are configured globally via enterprise policy (*). |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyTest, |
| BrowserTargetAllowedForComponentTrustedExtensionWithPolicyBlockedHosts) { |
| // Set up runtime blocked hosts globally for all extensions. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://*"); |
| } |
| |
| scoped_refptr<const Extension> component_trusted_extension = |
| ExtensionBuilder("Some Trusted Extension") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kComponent) |
| .AddAPIPermission("debugger") |
| .Build(); |
| PermissionsUpdater(profile()).InitializePermissions( |
| component_trusted_extension.get()); |
| // Verify that policy blocked hosts were populated from enterprise policy. |
| ASSERT_FALSE(component_trusted_extension->permissions_data() |
| ->policy_blocked_hosts() |
| .is_empty()); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(component_trusted_extension.get()); |
| |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile())) |
| << attach_function->GetError(); |
| |
| // Clean up and detach. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(component_trusted_extension.get()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), R"([{"targetId": "browser"}])", profile())); |
| } |
| |
| // Tests that an unpacked trusted extension is exempt from the |
| // policy_blocked_hosts check when the allow unpacked switch is set. |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyTest, |
| BrowserTargetAllowedForUnpackedTrustedExtensionWithFlagAndPolicyBlockedHosts) { |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| ::switches::kAllowUnpackedPerfettoExtension); |
| |
| // Set up runtime blocked hosts globally for all extensions. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://*"); |
| } |
| |
| scoped_refptr<const Extension> unpacked_trusted_extension = |
| ExtensionBuilder("Some Trusted Extension") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kUnpacked) |
| .AddAPIPermission("debugger") |
| .Build(); |
| PermissionsUpdater(profile()).InitializePermissions( |
| unpacked_trusted_extension.get()); |
| // Verify that policy blocked hosts were populated from enterprise policy. |
| ASSERT_FALSE(unpacked_trusted_extension->permissions_data() |
| ->policy_blocked_hosts() |
| .is_empty()); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(unpacked_trusted_extension.get()); |
| |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile())) |
| << attach_function->GetError(); |
| |
| // Clean up and detach. |
| auto detach_function = base::MakeRefCounted<DebuggerDetachFunction>(); |
| detach_function->set_extension(unpacked_trusted_extension.get()); |
| EXPECT_TRUE(api_test_utils::RunFunction( |
| detach_function.get(), R"([{"targetId": "browser"}])", profile())); |
| } |
| |
| // Tests that an unpacked extension without the command line flag is not |
| // treated as trusted and cannot attach to the browser target when policy |
| // blocked hosts are configured. |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyTest, |
| BrowserTargetNotAllowedForUnpackedTrustedExtensionWithPolicyBlockedHosts) { |
| // Set up runtime blocked hosts globally for all extensions. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://*"); |
| } |
| |
| scoped_refptr<const Extension> unpacked_trusted_extension = |
| ExtensionBuilder("Some Trusted Extension") |
| .SetID(extension_misc::kPerfettoUIExtensionId) |
| .SetLocation(mojom::ManifestLocation::kUnpacked) |
| .AddAPIPermission("debugger") |
| .Build(); |
| PermissionsUpdater(profile()).InitializePermissions( |
| unpacked_trusted_extension.get()); |
| // Verify that policy blocked hosts were populated from enterprise policy. |
| ASSERT_FALSE(unpacked_trusted_extension->permissions_data() |
| ->policy_blocked_hosts() |
| .is_empty()); |
| |
| auto attach_function = base::MakeRefCounted<DebuggerAttachFunction>(); |
| attach_function->set_extension(unpacked_trusted_extension.get()); |
| |
| std::string actual_error = api_test_utils::RunFunctionAndReturnError( |
| attach_function.get(), R"([{"targetId": "browser"}, "1.1"])", profile()); |
| |
| EXPECT_EQ("Host access is restricted by policy.", actual_error); |
| } |
| |
| // Test fixture for verifying chrome.debugger screenshot policy behavior when |
| // the kExtensionDebuggerStrictPolicyRestrictions feature flag is disabled |
| // (crbug.com/561948316). |
| class SitePerProcessDebuggerExtensionApiStrictPolicyDisabledTest |
| : public SitePerProcessDebuggerExtensionApiTest { |
| public: |
| SitePerProcessDebuggerExtensionApiStrictPolicyDisabledTest() { |
| scoped_feature_list_.InitAndDisableFeature( |
| extensions_features::kExtensionDebuggerStrictPolicyRestrictions); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that when kExtensionDebuggerStrictPolicyRestrictions is disabled |
| // (crbug.com/561948316), attaching the debugger is allowed even if enterprise |
| // policy disables screenshots globally. |
| IN_PROC_BROWSER_TEST_F( |
| SitePerProcessDebuggerExtensionApiStrictPolicyDisabledTest, |
| AttachAllowedWhenScreenshotsDisabled) { |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true); |
| |
| ASSERT_TRUE(RunExtensionTest("debugger_disable_screenshots", |
| {.custom_arg = "debuggerAllowed"})) |
| << message_; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Tests that when kExtensionDebuggerStrictPolicyRestrictions is disabled |
| // (crbug.com/561948316), attaching the debugger is allowed even if DLP |
| // restricts screenshots on the target. |
| IN_PROC_BROWSER_TEST_F( |
| SitePerProcessDebuggerExtensionApiStrictPolicyDisabledTest, |
| AttachAllowedWhenScreenshotsRestrictedByDlp) { |
| ASSERT_TRUE(embedded_test_server()->Started()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| policy::MockDlpContentManager mock_dlp_content_manager; |
| policy::ScopedDlpContentObserverForTesting scoped_dlp_content_observer( |
| &mock_dlp_content_manager); |
| EXPECT_CALL(mock_dlp_content_manager, |
| IsScreenshotApiRestricted(web_contents())) |
| .WillRepeatedly(testing::Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest("debugger_disable_screenshots", |
| {.custom_arg = "debuggerAllowed"})) |
| << message_; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Test fixture for verifying chrome.debugger runtime_blocked_hosts policy |
| // behavior when the kExtensionDebuggerStrictPolicyRestrictions feature flag is |
| // disabled (crbug.com/561948316). |
| class DebuggerExtensionManagementPolicyStrictPolicyDisabledTest |
| : public DebuggerExtensionManagementPolicyTestBase { |
| public: |
| DebuggerExtensionManagementPolicyStrictPolicyDisabledTest() { |
| scoped_feature_list_.InitAndDisableFeature( |
| extensions_features::kExtensionDebuggerStrictPolicyRestrictions); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that when kExtensionDebuggerStrictPolicyRestrictions is disabled |
| // (crbug.com/561948316), attaching the debugger to an unblocked host succeeds |
| // even when enterprise policy blocks another host (restoring legacy per-URL |
| // behavior). |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyStrictPolicyDisabledTest, |
| AttachAllowedWithPolicyBlockedHostsGlobal_UnblockedHost) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("b.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| // Set up runtime blocked hosts globally for all extensions (blocking a.test). |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://a.test"); |
| } |
| |
| // Attempting to attach to an unblocked host (b.test) succeeds. |
| ASSERT_TRUE(RunExtensionTest("debugger_policy_blocked_hosts")) << message_; |
| } |
| |
| // Tests that when kExtensionDebuggerStrictPolicyRestrictions is disabled |
| // (crbug.com/561948316), attaching the debugger directly to a policy-blocked |
| // host is still rejected with the legacy per-URL policy error. |
| IN_PROC_BROWSER_TEST_F( |
| DebuggerExtensionManagementPolicyStrictPolicyDisabledTest, |
| AttachRejectedWithPolicyBlockedHostsGlobal_BlockedHost) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL url = embedded_test_server()->GetURL("a.test", "/english_page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents(), url)); |
| |
| // Set up runtime blocked hosts globally for all extensions (blocking a.test). |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://a.test"); |
| } |
| |
| // Attempting to attach to the blocked host (a.test) fails with the legacy |
| // per-URL error message. |
| ASSERT_TRUE(RunExtensionTest( |
| "debugger_policy_blocked_hosts", |
| {.custom_arg = |
| "This page cannot be scripted due to an ExtensionsSettings policy."})) |
| << message_; |
| } |
| |
| } // namespace extensions |