| // 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 <array> |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/to_string.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/run_until.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/test/with_feature_override.h" |
| #include "base/time/time.h" |
| #include "base/time/time_override.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h" |
| #include "chrome/browser/devtools/url_constants.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/error_console/error_console.h" |
| #include "chrome/browser/extensions/error_console/error_console_test_observer.h" |
| #include "chrome/browser/extensions/extension_action_runner.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_browser_test_util.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/extension_with_management_policy_apitest.h" |
| #include "chrome/browser/extensions/permissions/active_tab_permission_granter.h" |
| #include "chrome/browser/extensions/permissions/scripting_permissions_modifier.h" |
| #include "chrome/browser/net/profile_network_context_service.h" |
| #include "chrome/browser/net/profile_network_context_service_factory.h" |
| #include "chrome/browser/net/system_network_context_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_destroyer.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/extensions/reload_page_dialog_controller.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/test/base/search_test_utils.h" |
| #include "components/embedder_support/switches.h" |
| #include "components/google/core/common/google_switches.h" |
| #include "components/policy/core/common/mock_configuration_policy_provider.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/proxy_config/proxy_config_dictionary.h" |
| #include "components/proxy_config/proxy_config_pref_names.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "components/web_package/web_bundle_builder.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/webui_config_map.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/page_type.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/prerender_test_util.h" |
| #include "content/public/test/simple_url_loader_test_helper.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "content/public/test/url_loader_monitor.h" |
| #include "content/public/test/web_transport_simple_test_server.h" |
| #include "extensions/browser/api/web_request/extension_web_request_event_router.h" |
| #include "extensions/browser/api/web_request/web_request_api.h" |
| #include "extensions/browser/background_script_executor.h" |
| #include "extensions/browser/blocked_action_type.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registrar.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/install_prefs_helper.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/service_worker/service_worker_task_queue.h" |
| #include "extensions/browser/service_worker/service_worker_test_utils.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/features/feature.h" |
| #include "extensions/common/mojom/event_router.mojom-test-utils.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "google_apis/gaia/gaia_switches.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/features.h" |
| #include "net/base/network_isolation_key.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_util.h" |
| #include "net/test/embedded_test_server/default_handlers.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "net/test/embedded_test_server/request_handler_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/metrics/public/cpp/metrics_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/metrics/public/mojom/ukm_interface.mojom.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/cpp/url_loader_factory_builder.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/input/web_input_event.h" |
| #include "third_party/blink/public/common/service_worker/service_worker_status_code.h" |
| #include "ui/webui/untrusted_web_ui_browsertest_util.h" // nogncheck |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list.h" |
| #include "chrome/test/base/android/android_ui_test_utils.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "chrome/browser/extensions/test_extension_action_dispatcher_observer.h" |
| #include "chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader.h" |
| #include "chrome/browser/new_tab_page/one_google_bar/one_google_bar_service.h" |
| #include "chrome/browser/new_tab_page/one_google_bar/one_google_bar_service_factory.h" |
| #include "chrome/browser/search/search.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" // nogncheck |
| #include "chrome/browser/ui/login/login_handler.h" // nogncheck |
| #include "chrome/browser/ui/search/ntp_test_utils.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "ui/base/ui_base_features.h" |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| using content::WebContents; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // This is the public key of tools/origin_trials/eftest.key, used to validate |
| // origin trial tokens generated by tools/origin_trials/generate_token.py. |
| constexpr char kOriginTrialPublicKeyForTesting[] = |
| "dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA="; |
| |
| // Observer that listens for messages from chrome.test.sendMessage to allow them |
| // to be used to trigger browser initiated naviagations from the javascript for |
| // testing purposes. |
| class NavigateTabMessageHandler { |
| public: |
| explicit NavigateTabMessageHandler(Profile* profile) : profile_(profile) { |
| navigate_listener_.SetOnRepeatedlySatisfied(base::BindRepeating( |
| &NavigateTabMessageHandler::HandleNavigateTabMessage, |
| base::Unretained(this))); |
| } |
| |
| ~NavigateTabMessageHandler() = default; |
| |
| private: |
| void HandleNavigateTabMessage(const std::string& message) { |
| std::optional<base::Value> command = |
| base::JSONReader::Read(message, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| if (command && command->is_dict()) { // Check the message decoded from JSON |
| base::Value::Dict* data = command->GetDict().FindDict("navigate"); |
| if (data) { |
| int tab_id = data->FindInt("tabId").value(); |
| GURL url = GURL(*data->FindString("url")); |
| ASSERT_TRUE(url.is_valid()); |
| |
| content::WebContents* contents = nullptr; |
| ExtensionTabUtil::GetTabById( |
| tab_id, profile_, profile_->HasPrimaryOTRProfile(), &contents); |
| ASSERT_NE(contents, nullptr) |
| << "Could not find tab with id: " << tab_id; |
| content::NavigationController::LoadURLParams params(url); |
| contents->GetController().LoadURLWithParams(params); |
| } |
| } |
| navigate_listener_.Reset(); |
| } |
| |
| raw_ptr<Profile, DanglingUntriaged> profile_; |
| ExtensionTestMessageListener navigate_listener_; |
| }; |
| |
| // A helper class that intercepts the |
| // `EventRouter::RemoveListenerForServiceWorker()` mojom receiver method and |
| // does *not* forward the call onto the real `EventRouter` browser |
| // implementation. |
| class EventRouterInterceptorForStopListenerRemoval |
| : public mojom::EventRouterInterceptorForTesting { |
| public: |
| EventRouterInterceptorForStopListenerRemoval( |
| content::BrowserContext* browser_context, |
| int worker_renderer_process_id) |
| : browser_context_(browser_context) { |
| auto* event_router = extensions::EventRouter::Get(browser_context_); |
| CHECK(event_router) << "There is no EventRouter for browser context when " |
| "creating the event router interceptor."; |
| event_router->SwapReceiverForTesting(worker_renderer_process_id, this); |
| } |
| |
| mojom::EventRouter* GetForwardingInterface() override { |
| // This should be non-null if this interface is still receiving events. This |
| // causes all methods other than `RemoveListenerForServiceWorker()` to be |
| // sent along to the real implementation. |
| auto* event_router = extensions::EventRouter::Get(browser_context_); |
| CHECK(event_router) |
| << "There is no `EventRouter` for browser context when attempting to " |
| "forward a mojom call to the real `EventRouter` implementation"; |
| return event_router; |
| } |
| |
| protected: |
| // mojom::EventRouter: |
| void RemoveListenerForServiceWorker( |
| mojom::EventListenerPtr event_listener) override { |
| // Don't call the real `EventRouter::RemoveListenerForServiceWorker()` |
| // method to simulate that the worker never finishing stopping and informing |
| // the browser to remove the listener. |
| } |
| |
| private: |
| raw_ptr<content::BrowserContext> browser_context_; |
| }; |
| |
| constexpr char kGetNumRequests[] = |
| R"((async function() { |
| // Wait for any pending storage writes to complete. |
| await flushStorage(); |
| chrome.storage.local.get( |
| {requestCount: -1}, |
| (result) => { |
| chrome.test.sendScriptResult(result.requestCount); |
| }); |
| })();)"; |
| |
| // Sends an XHR request to the provided host, port, and path, and responds when |
| // the request was sent. |
| const char kPerformXhrJs[] = |
| "var url = 'http://%s:%d/%s';\n" |
| "var xhr = new XMLHttpRequest();\n" |
| "xhr.open('GET', url);\n" |
| "new Promise(resolve => {" |
| " xhr.onload = function() {\n" |
| " resolve(true);\n" |
| " };\n" |
| " xhr.onerror = function() {\n" |
| " resolve(false);\n" |
| " };\n" |
| " xhr.send();\n" |
| "});\n"; |
| |
| // Header values set by the server and by the extension. |
| const char kHeaderValueFromExtension[] = "ValueFromExtension"; |
| const char kHeaderValueFromServer[] = "ValueFromServer"; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| constexpr char kCORSUrl[] = "http://cors.test/cors"; |
| constexpr char kCORSProxyUser[] = "testuser"; |
| constexpr char kCORSProxyPass[] = "testpass"; |
| constexpr char kCustomPreflightHeader[] = "x-testheader"; |
| #endif |
| |
| // Performs an XHR in the given |frame|, replying when complete. |
| void PerformXhrInFrame(content::RenderFrameHost* frame, |
| const std::string& host, |
| int port, |
| const std::string& page) { |
| EXPECT_EQ(true, EvalJs(frame, base::StringPrintf(kPerformXhrJs, host.c_str(), |
| port, page.c_str()))); |
| } |
| |
| base::Value ExecuteScriptAndReturnValue(const ExtensionId& extension_id, |
| content::BrowserContext* context, |
| const std::string& script) { |
| return BackgroundScriptExecutor::ExecuteScript( |
| context, extension_id, script, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| std::optional<bool> ExecuteScriptAndReturnBool(const ExtensionId& extension_id, |
| content::BrowserContext* context, |
| const std::string& script) { |
| std::optional<bool> result; |
| base::Value script_result = |
| ExecuteScriptAndReturnValue(extension_id, context, script); |
| if (script_result.is_bool()) { |
| result = script_result.GetBool(); |
| } |
| return result; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| std::optional<std::string> ExecuteScriptAndReturnString( |
| const ExtensionId& extension_id, |
| content::BrowserContext* context, |
| const std::string& script) { |
| std::optional<std::string> result; |
| base::Value script_result = |
| ExecuteScriptAndReturnValue(extension_id, context, script); |
| if (script_result.is_string()) { |
| result = script_result.GetString(); |
| } |
| return result; |
| } |
| |
| // Returns the current count of a variable stored in the |extension| background |
| // script context (either background page or service worker). Returns -1 if |
| // something goes awry. |
| int GetCountFromBackgroundScript(const Extension* extension, |
| content::BrowserContext* context, |
| const std::string& variable_name) { |
| const std::string script = base::StringPrintf( |
| "chrome.test.sendScriptResult(%s)", variable_name.c_str()); |
| base::Value value = |
| ExecuteScriptAndReturnValue(extension->id(), context, script); |
| if (!value.is_int()) { |
| return -1; |
| } |
| return value.GetInt(); |
| } |
| |
| // Returns the current count of webRequests received by the |extension| in |
| // the background script, either background page or service worker. Assumes the |
| // extension stores a value on the `self` object. Returns -1 if something goes |
| // awry. |
| int GetWebRequestCountFromBackgroundScript(const Extension* extension, |
| content::BrowserContext* context) { |
| return GetCountFromBackgroundScript(extension, context, |
| "self.webRequestCount"); |
| } |
| |
| // Returns true if the |extension|'s background script saw an event for a |
| // request with the given |hostname| (|hostname| should exclude port). |
| bool HasSeenWebRequestInBackgroundScript(const Extension* extension, |
| content::BrowserContext* context, |
| const std::string& hostname) { |
| const std::string script = base::StringPrintf( |
| R"(chrome.test.sendScriptResult( |
| self.requestedHostnames.includes('%s'));)", |
| hostname.c_str()); |
| base::Value value = |
| ExecuteScriptAndReturnValue(extension->id(), context, script); |
| DCHECK(value.is_bool()); |
| return value.GetBool(); |
| } |
| |
| void WaitForExtraHeadersListener(base::WaitableEvent* event, |
| content::BrowserContext* browser_context) { |
| if (BrowserContextKeyedAPIFactory<WebRequestAPI>::Get(browser_context) |
| ->HasExtraHeadersListenerForTesting()) { |
| event->Signal(); |
| return; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&WaitForExtraHeadersListener, event, browser_context)); |
| } |
| |
| } // namespace |
| |
| class ExtensionWebRequestApiTest : public ExtensionApiTest { |
| public: |
| explicit ExtensionWebRequestApiTest( |
| ContextType context_type = ContextType::kFromManifest) |
| : ExtensionApiTest(context_type) { |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, |
| // TODO(crbug.com/40248833): Use HTTPS URLs in tests to avoid having to |
| // disable this feature. |
| /*disabled_features=*/ |
| {features::kHttpsUpgrades, features::kHttpsFirstModeIncognito}); |
| } |
| ExtensionWebRequestApiTest(const ExtensionWebRequestApiTest&) = delete; |
| ExtensionWebRequestApiTest& operator=(const ExtensionWebRequestApiTest&) = |
| delete; |
| ~ExtensionWebRequestApiTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| navigation_handler_ = |
| std::make_unique<NavigateTabMessageHandler>(profile()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII(switches::kGaiaUrl, "http://gaia.com"); |
| command_line->AppendSwitchASCII(embedder_support::kOriginTrialPublicKey, |
| kOriginTrialPublicKeyForTesting); |
| } |
| |
| void RunPermissionTest(const char* extension_directory, |
| bool load_extension_with_incognito_permission, |
| bool wait_for_extension_loaded_in_incognito, |
| const char* expected_content_regular_window, |
| const char* exptected_content_incognito_window, |
| ContextType context_type); |
| |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| CreateURLLoaderFactory() { |
| network::mojom::URLLoaderFactoryParamsPtr params = |
| network::mojom::URLLoaderFactoryParams::New(); |
| params->process_id = network::mojom::kBrowserProcessId; |
| params->automatically_assign_isolation_info = true; |
| params->is_orb_enabled = false; |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> loader_factory; |
| profile() |
| ->GetDefaultStoragePartition() |
| ->GetNetworkContext() |
| ->CreateURLLoaderFactory( |
| loader_factory.InitWithNewPipeAndPassReceiver(), std::move(params)); |
| return loader_factory; |
| } |
| |
| void InstallWebRequestExtension(const std::string& name) { |
| constexpr char kManifest[] = R"({ |
| "name": "%s", |
| "version": "1", |
| "manifest_version": 2, |
| "permissions": [ |
| "webRequest" |
| ] |
| })"; |
| TestExtensionDir dir; |
| dir.WriteManifest(base::StringPrintf(kManifest, name.c_str())); |
| LoadExtension(dir.UnpackedPath()); |
| test_dirs_.push_back(std::move(dir)); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| std::vector<TestExtensionDir> test_dirs_; |
| std::unique_ptr<NavigateTabMessageHandler> navigation_handler_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| WebRequestApiClearsBindingOnFirstListener) { |
| // Skip if the proxy is forced since the bindings will never be cleared in |
| // that case. |
| if (base::FeatureList::IsEnabled( |
| extensions_features::kForceWebRequestProxyForTest)) { |
| return; |
| } |
| |
| mojo::Remote<network::mojom::URLLoaderFactory> loader_factory( |
| CreateURLLoaderFactory()); |
| bool has_connection_error = false; |
| loader_factory.set_disconnect_handler( |
| base::BindLambdaForTesting([&]() { has_connection_error = true; })); |
| InstallWebRequestExtension("extension1"); |
| profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting(); |
| EXPECT_TRUE(has_connection_error); |
| loader_factory.reset(); |
| |
| // The second time there should be no connection error. |
| loader_factory.Bind(CreateURLLoaderFactory()); |
| has_connection_error = false; |
| loader_factory.set_disconnect_handler( |
| base::BindLambdaForTesting([&]() { has_connection_error = true; })); |
| InstallWebRequestExtension("extension2"); |
| profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting(); |
| EXPECT_FALSE(has_connection_error); |
| } |
| |
| // Tests registering webRequest events in multiple contexts in the same |
| // extension (which will thus be in the same process). Regression test for |
| // https://crbug.com/1297276. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| ListenersInMultipleContexts) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Load an extension that has a page with two iframes. Each iframe registers |
| // a listener for the same event. |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "ext", |
| "manifest_version": 3, |
| "version": "1", |
| "permissions": ["webRequest"], |
| "host_permissions": ["http://example.com/*"] |
| })"; |
| static constexpr char kParentHtml[] = |
| R"(<!doctype html> |
| <html> |
| Hello world |
| <iframe src="iframe.html" name="iframe1"></iframe> |
| <iframe src="iframe.html" name="iframe2"></iframe> |
| </html>)"; |
| static constexpr char kIframeHtml[] = |
| R"(<!doctype html> |
| <html> |
| Iframe |
| <script src="iframe.js"></script> |
| </html>)"; |
| static constexpr char kIframeJs[] = |
| R"(const frameName = window.name; |
| chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| chrome.test.sendMessage(frameName + ' event'); |
| }, |
| {urls: ['http://example.com/*'], types: ['main_frame']}); |
| chrome.test.sendMessage(frameName + ' ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("parent.html"), kParentHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("iframe.html"), kIframeHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("iframe.js"), kIframeJs); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| auto* router = WebRequestEventRouter::Get(profile()); |
| ASSERT_TRUE(router); |
| |
| static constexpr char kEventName[] = "webRequest.onBeforeRequest"; |
| EXPECT_EQ(0u, router->GetListenerCountForTesting(profile(), kEventName)); |
| |
| // Load the extension page and wait for it to register its listeners. |
| { |
| ExtensionTestMessageListener listener1("iframe1 ready"); |
| ExtensionTestMessageListener listener2("iframe2 ready"); |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| extension->GetResourceURL("parent.html"))); |
| ASSERT_TRUE(listener1.WaitUntilSatisfied()); |
| ASSERT_TRUE(listener2.WaitUntilSatisfied()); |
| } |
| |
| // Two different listeners should be registered. |
| EXPECT_EQ(2u, router->GetListenerCountForTesting(profile(), kEventName)); |
| |
| // Trigger an event. Both listeners should fire. |
| { |
| ExtensionTestMessageListener listener1("iframe1 event"); |
| ExtensionTestMessageListener listener2("iframe2 event"); |
| NavigateToURLInNewTab( |
| embedded_test_server()->GetURL("example.com", "/title1.html")); |
| EXPECT_TRUE(listener1.WaitUntilSatisfied()); |
| EXPECT_TRUE(listener2.WaitUntilSatisfied()); |
| } |
| } |
| |
| // Regression test for https://crbug.com/395985663. |
| // TODO(crbug.com/399261153): Flaky on Android. |
| #if !BUILDFLAG(IS_ANDROID) |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| ExtensionRequestRedirectToServer) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Extension request redirect to server", |
| "manifest_version": 2, |
| "version": "0.1", |
| "default_locale": "en", |
| "background": { "scripts": ["background.js"] }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| content::JsReplace( |
| R"( |
| chrome.webRequest.onBeforeRequest.addListener( |
| function(details) { |
| if (details.url.endsWith('test') && |
| details.url.startsWith('chrome-extension')) { |
| // Redirect "test" chrome-extension:// URL request to the |
| // HTTP server URL. |
| return {redirectUrl: $1}; |
| } |
| }, |
| {urls: ['<all_urls>']}, |
| ['blocking']); |
| |
| (async () => { |
| const res = await fetch('./test'); |
| const text = await res.text(); |
| const expected = 'p {\n color: __MSG_text_color__;\n}\n'; |
| // "__MSG_text_color__" must not be replaced with "red". |
| if (text == expected) { |
| chrome.test.notifyPass(); |
| } else { |
| chrome.test.notifyFail('Unexpected content :' + text); |
| } |
| })(); |
| )", |
| embedded_test_server()->GetURL( |
| "/extensions/api_test/content_scripts/css_l10n/test.css"))); |
| |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| ASSERT_TRUE( |
| base::CopyDirectory(test_data_dir_.AppendASCII("content_scripts") |
| .AppendASCII("css_l10n") |
| .AppendASCII("_locales"), |
| test_dir.UnpackedPath(), true)); |
| } |
| ResultCatcher result_catcher; |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| using ContextType = extensions::browser_test_util::ContextType; |
| |
| enum class BackgroundResourceFetchTestCase { |
| kBackgroundResourceFetchEnabled, |
| kBackgroundResourceFetchDisabled, |
| }; |
| |
| class ExtensionWebRequestApiTestWithContextType |
| : public ExtensionWebRequestApiTest, |
| public testing::WithParamInterface< |
| std::pair<ContextType, BackgroundResourceFetchTestCase>> { |
| public: |
| ExtensionWebRequestApiTestWithContextType() |
| : ExtensionWebRequestApiTest(GetParam().first) { |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| // TODO(crbug.com/395895368): the right fix is to set |
| // network::switches::kIpAddressSpaceOverrides command line override, but |
| // this is complicated by the fact that the different tests start the |
| // embedded test server in different ways in the tests themselves, whereas |
| // this command line switch needs to be set after the embedded test server |
| // starts to obtain the correct port. Refactor tests to ensure the ones that |
| // need the command line switch override are within a specific test class. |
| disabled_features.push_back(network::features::kLocalNetworkAccessChecks); |
| if (IsBackgroundResourceFetchEnabled()) { |
| enabled_features.push_back(blink::features::kBackgroundResourceFetch); |
| } else { |
| disabled_features.push_back(blink::features::kBackgroundResourceFetch); |
| } |
| feature_background_resource_fetch_.InitWithFeatures(enabled_features, |
| disabled_features); |
| } |
| ExtensionWebRequestApiTestWithContextType( |
| const ExtensionWebRequestApiTestWithContextType&) = delete; |
| ExtensionWebRequestApiTestWithContextType& operator=( |
| const ExtensionWebRequestApiTestWithContextType&) = delete; |
| ~ExtensionWebRequestApiTestWithContextType() override = default; |
| |
| struct PrintToStringParamName { |
| std::string operator()( |
| const testing::TestParamInfo< |
| std::pair<ContextType, BackgroundResourceFetchTestCase>>& info) |
| const { |
| switch (info.param.second) { |
| case BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled: |
| return "BackgroundResourceFetchEnabled"; |
| case BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled: |
| return "BackgroundResourceFetchDisabled"; |
| } |
| } |
| }; |
| |
| protected: |
| ContextType GetContextType() const { return GetParam().first; } |
| |
| private: |
| bool IsBackgroundResourceFetchEnabled() const { |
| return GetParam().second == |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled; |
| } |
| base::test::ScopedFeatureList feature_background_resource_fetch_; |
| }; |
| |
| // Tests that use this class are checking for a sub-resource HSTS upgrade. |
| // Because kHstsTopLevelNavigationsOnly explicitly disallows sub-resource |
| // upgrades it needs to be disabled for these tests to pass. |
| class ExtensionWebRequestApiTestWithContextTypeForHstsTopLevelNavigationOnly |
| : public ExtensionWebRequestApiTestWithContextType { |
| public: |
| ExtensionWebRequestApiTestWithContextTypeForHstsTopLevelNavigationOnly() { |
| scoped_feature_list_.InitAndDisableFeature( |
| net::features::kHstsTopLevelNavigationsOnly); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // This test suite verifies extension functionality in the service worker |
| // context, required for Manifest V3 and later. |
| // |
| // The test harness attempts to automatically convert Manifest V2 extensions to |
| // Manifest V3 for testing in ContextType::kServiceWorker. However, some |
| // Manifest V2 features are incompatible with Manifest V3 (e.g., |
| // `webRequestBlocking`) and prevent automatic conversion. Extensions using |
| // these features must be manually migrated to Manifest V3 to be included in |
| // this test suite. |
| class ExtensionWebRequestApiTestWithContextTypeMV3 |
| : public ExtensionWebRequestApiTestWithContextType { |
| public: |
| ExtensionWebRequestApiTestWithContextTypeMV3() = default; |
| ExtensionWebRequestApiTestWithContextTypeMV3( |
| const ExtensionWebRequestApiTestWithContextTypeMV3&) = delete; |
| ExtensionWebRequestApiTestWithContextTypeMV3& operator=( |
| const ExtensionWebRequestApiTestWithContextTypeMV3&) = delete; |
| ~ExtensionWebRequestApiTestWithContextTypeMV3() override = default; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionWebRequestApiTestWithContextTypeMV3, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorker, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorker, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextTypeMV3, |
| WebRequestApi) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_api")) << message_; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ExtensionWebRequestApiTestWithContextType, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionWebRequestApiTestWithContextType, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ExtensionWebRequestApiTestWithContextTypeForHstsTopLevelNavigationOnly, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionWebRequestApiTestWithContextTypeForHstsTopLevelNavigationOnly, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| class DevToolsFrontendInWebRequestApiTest : public ExtensionApiTest { |
| public: |
| DevToolsFrontendInWebRequestApiTest() { |
| // TODO(crbug.com/40248833): Use HTTPS URLs in tests to avoid having to |
| // disable this feature. |
| feature_list_.InitAndDisableFeature(features::kHttpsUpgrades); |
| } |
| |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| int port = embedded_test_server()->port(); |
| |
| url_loader_interceptor_ = std::make_unique<content::URLLoaderInterceptor>( |
| base::BindRepeating(&DevToolsFrontendInWebRequestApiTest::OnIntercept, |
| base::Unretained(this), port)); |
| |
| navigation_handler_ = |
| std::make_unique<NavigateTabMessageHandler>(profile()); |
| } |
| |
| void TearDownOnMainThread() override { |
| url_loader_interceptor_.reset(); |
| ExtensionApiTest::TearDownOnMainThread(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| |
| test_root_dir_ = test_data_dir_.AppendASCII("webrequest"); |
| |
| embedded_test_server()->ServeFilesFromDirectory(test_root_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| command_line->AppendSwitchASCII( |
| switches::kCustomDevtoolsFrontend, |
| embedded_test_server() |
| ->GetURL("customfrontend.example.com", "/devtoolsfrontend/") |
| .spec()); |
| } |
| |
| private: |
| bool OnIntercept(int test_server_port, |
| content::URLLoaderInterceptor::RequestParams* params) { |
| // The devtools remote frontend URLs are hardcoded into Chrome and are |
| // requested by some of the tests here to exercise their behavior with |
| // respect to WebRequest. |
| // |
| // We treat any URL request not targeting the test server as targeting the |
| // remote frontend, and we intercept them to fulfill from test data rather |
| // than hitting the network. |
| if (params->url_request.url.EffectiveIntPort() == test_server_port) { |
| return false; |
| } |
| |
| std::string status_line; |
| std::string contents; |
| GetFileContents( |
| test_root_dir_.AppendASCII(params->url_request.url.GetPath().substr(1)), |
| &status_line, &contents); |
| content::URLLoaderInterceptor::WriteResponse(status_line, contents, |
| params->client.get()); |
| return true; |
| } |
| |
| static void GetFileContents(const base::FilePath& path, |
| std::string* status_line, |
| std::string* contents) { |
| base::ScopedAllowBlockingForTesting allow_io; |
| if (!base::ReadFileToString(path, contents)) { |
| *status_line = "HTTP/1.0 404 Not Found\n\n"; |
| return; |
| } |
| |
| std::string content_type; |
| if (path.Extension() == FILE_PATH_LITERAL(".html")) { |
| content_type = "Content-type: text/html\n"; |
| } else if (path.Extension() == FILE_PATH_LITERAL(".js")) { |
| content_type = "Content-type: application/javascript\n"; |
| } |
| |
| *status_line = |
| base::StringPrintf("HTTP/1.0 200 OK\n%s\n", content_type.c_str()); |
| } |
| |
| base::test::ScopedFeatureList feature_list_; |
| base::FilePath test_root_dir_; |
| std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_; |
| std::unique_ptr<NavigateTabMessageHandler> navigation_handler_; |
| }; |
| |
| // Ensure that devtools frontend requests are hidden from the webRequest API. |
| IN_PROC_BROWSER_TEST_F(DevToolsFrontendInWebRequestApiTest, HiddenRequests) { |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_devtools.html"})) |
| << message_; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestApi) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_api")) << message_; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestSimple) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_simple")) << message_; |
| } |
| |
| // TODO(crbug.com/333791060): Parameterized test is flaky on multiple bots. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestComplex) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_complex")) << message_; |
| } |
| |
| class ExtensionDevToolsProtocolTest |
| : public ExtensionWebRequestApiTestWithContextType, |
| public content::TestDevToolsProtocolClient { |
| protected: |
| void Attach() { AttachToWebContents(GetActiveWebContents()); } |
| |
| void TearDownOnMainThread() override { |
| DetachProtocolClient(); |
| ExtensionWebRequestApiTest::TearDownOnMainThread(); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ExtensionDevToolsProtocolTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionDevToolsProtocolTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionDevToolsProtocolTest, |
| HeaderOverriddenByExtension) { |
| Attach(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Header Override Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| var headers = details.responseHeaders; |
| headers.push({name: "extensionHeaderName", |
| value: "extensionHeaderValue"}); |
| return {responseHeaders: headers}; |
| }, |
| {urls: ['<all_urls>']}, |
| ['responseHeaders', 'extraHeaders', 'blocking']); |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| SendCommand("Network.enable", base::Value::Dict(), true); |
| const GURL url( |
| embedded_test_server()->GetURL("/set-cookie?cookieName=cookieValue")); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, WindowOpenDisposition::CURRENT_TAB, |
| ui_test_utils::BROWSER_TEST_NO_WAIT); |
| |
| // Check that `Network.responseReceived` contains the response header added |
| // by the extension |
| base::Value::Dict response_received_result = |
| WaitForNotification("Network.responseReceived", false); |
| auto* extension_header = response_received_result.FindByDottedPath( |
| "response.headers.extensionHeaderName"); |
| ASSERT_TRUE(extension_header); |
| ASSERT_EQ(*extension_header, "extensionHeaderValue"); |
| |
| // Check that the cookie as specified in the original headers has been set |
| auto* get_all_cookies_result = |
| SendCommand("Network.getAllCookies", base::Value::Dict(), true); |
| const base::Value::List* cookies = |
| get_all_cookies_result->FindList("cookies"); |
| ASSERT_TRUE(cookies); |
| ASSERT_EQ(cookies->size(), 1u); |
| ASSERT_TRUE(cookies->front().is_dict()); |
| auto* cookie_name = cookies->front().GetDict().FindString("name"); |
| ASSERT_TRUE(cookie_name); |
| ASSERT_EQ(*cookie_name, "cookieName"); |
| auto* cookie_value = cookies->front().GetDict().FindString("value"); |
| ASSERT_TRUE(cookie_value); |
| ASSERT_EQ(*cookie_value, "cookieValue"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionDevToolsProtocolTest, |
| HeaderOverrideViaProtocolAllowedByExtension) { |
| Attach(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Header Override Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| var headers = details.responseHeaders; |
| headers.push({name: "extensionHeaderName", |
| value: "extensionHeaderValue"}); |
| return {responseHeaders: headers}; |
| }, |
| {urls: ['<all_urls>']}, |
| ['responseHeaders', 'extraHeaders', 'blocking']); |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| SendCommand("Network.enable", base::Value::Dict(), true); |
| |
| base::Value::Dict enable_params; |
| base::Value::List patterns; |
| base::Value::Dict pattern; |
| pattern.Set("requestStage", "Response"); |
| patterns.Append(std::move(pattern)); |
| enable_params.Set("patterns", std::move(patterns)); |
| SendCommand("Fetch.enable", std::move(enable_params), true); |
| |
| const GURL url( |
| embedded_test_server()->GetURL("/set-cookie?cookieName=cookieValue")); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, WindowOpenDisposition::CURRENT_TAB, |
| ui_test_utils::BROWSER_TEST_NO_WAIT); |
| base::Value::Dict request_paused_result = |
| WaitForNotification("Fetch.requestPaused", true); |
| std::string* request_id = request_paused_result.FindString("requestId"); |
| |
| // Checks that `Fetch.requestPaused` contains the response headers added by |
| // the extension |
| base::Value::List* response_headers = |
| request_paused_result.FindListByDottedPath("responseHeaders"); |
| auto* header_name = response_headers->back().GetDict().FindString("name"); |
| ASSERT_TRUE(header_name); |
| ASSERT_EQ(*header_name, "extensionHeaderName"); |
| auto* header_value = response_headers->back().GetDict().FindString("value"); |
| ASSERT_TRUE(header_value); |
| ASSERT_EQ(*header_value, "extensionHeaderValue"); |
| |
| // Response headers are replaced by new overrides |
| base::Value::Dict params; |
| params.Set("requestId", *request_id); |
| base::Value::Dict header_1; |
| header_1.Set("name", "firstName"); |
| header_1.Set("value", "firstValue"); |
| base::Value::Dict header_2; |
| header_2.Set("name", "secondName"); |
| header_2.Set("value", "secondValue"); |
| base::Value::List headers; |
| headers.Append(std::move(header_1)); |
| headers.Append(std::move(header_2)); |
| params.Set("responseHeaders", std::move(headers)); |
| params.Set("responseCode", 200); |
| params.Set("body", ""); |
| SendCommand("Fetch.fulfillRequest", std::move(params), false); |
| |
| // Check that `Network.responseReceived` contains the response headers as |
| // specified via `Fetch.fulfillRequest` |
| base::Value::Dict response_received_result = |
| WaitForNotification("Network.responseReceived", false); |
| auto* first_header = |
| response_received_result.FindByDottedPath("response.headers.firstName"); |
| ASSERT_TRUE(first_header); |
| ASSERT_EQ(*first_header, "firstValue"); |
| auto* second_header = |
| response_received_result.FindByDottedPath("response.headers.secondName"); |
| ASSERT_TRUE(second_header); |
| ASSERT_EQ(*second_header, "secondValue"); |
| ASSERT_EQ(response_received_result.FindByDottedPath("response.headers") |
| ->GetDict() |
| .size(), |
| 2u); |
| |
| // Check that the cookie as specified in the original headers has been set |
| auto* get_all_cookies_result = |
| SendCommand("Network.getAllCookies", base::Value::Dict(), true); |
| const base::Value::List* cookies = |
| get_all_cookies_result->FindList("cookies"); |
| ASSERT_TRUE(cookies); |
| ASSERT_EQ(cookies->size(), 1u); |
| auto* cookie_name = cookies->front().GetDict().FindString("name"); |
| ASSERT_TRUE(cookie_name); |
| ASSERT_EQ(*cookie_name, "cookieName"); |
| auto* cookie_value = cookies->front().GetDict().FindString("value"); |
| ASSERT_TRUE(cookie_value); |
| ASSERT_EQ(*cookie_value, "cookieValue"); |
| } |
| |
| // TODO(crbug.com/40168662) The test is flaky on multiple bots. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, DISABLED_WebRequestTypes) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_types")) << message_; |
| } |
| |
| // Test that a request to an OpenSearch description document (OSDD) generates |
| // an event with the expected details. |
| // Flaky on Windows and Mac: https://crbug.com/1218893 |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) |
| #define MAYBE_WebRequestTestOSDD DISABLED_WebRequestTestOSDD |
| #else |
| #define MAYBE_WebRequestTestOSDD WebRequestTestOSDD |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_WebRequestTestOSDD) { |
| // An OSDD request is only generated when a main frame at is loaded at /, so |
| // serve osdd/index.html from the root of the test server: |
| embedded_test_server()->ServeFilesFromDirectory( |
| test_data_dir_.AppendASCII("webrequest/osdd")); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| search_test_utils::WaitForTemplateURLServiceToLoad( |
| TemplateURLServiceFactory::GetForProfile(profile())); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_osdd")) << message_; |
| } |
| |
| // Test that the webRequest events are dispatched with the expected details when |
| // a frame or tab is removed while a response is being received. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| WebRequestUnloadAfterRequest) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?1"})) |
| << message_; |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?2"})) |
| << message_; |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?3"})) |
| << message_; |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?4"})) |
| << message_; |
| } |
| |
| // Test that the webRequest events are dispatched with the expected details when |
| // a frame or tab is immediately removed after starting a request. |
| // Flaky on all platforms. See crbug.com/780369 for detail. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| DISABLED_WebRequestUnloadImmediately) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?5"})) |
| << message_; |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_unload.html?6"})) |
| << message_; |
| } |
| |
| enum class ProfileMode { |
| kUserProfile, |
| kIncognito, |
| }; |
| |
| struct ARTestParams { |
| ProfileMode profile_mode; |
| ContextType context_type; |
| }; |
| |
| class ExtensionWebRequestApiAuthRequiredTest |
| : public ExtensionWebRequestApiTest, |
| public testing::WithParamInterface<ARTestParams> { |
| public: |
| ExtensionWebRequestApiAuthRequiredTest() |
| : ExtensionWebRequestApiTest(GetParam().context_type) {} |
| ~ExtensionWebRequestApiAuthRequiredTest() override = default; |
| ExtensionWebRequestApiAuthRequiredTest( |
| const ExtensionWebRequestApiAuthRequiredTest&) = delete; |
| ExtensionWebRequestApiAuthRequiredTest& operator=( |
| ExtensionWebRequestApiAuthRequiredTest&) = delete; |
| |
| protected: |
| static bool GetEnableIncognito() { |
| return GetParam().profile_mode == ProfileMode::kIncognito; |
| } |
| |
| static std::string FormatCustomArg(const char* test_name) { |
| static constexpr char custom_arg_format[] = |
| R"({"testName": "%s", "runInIncognito": %s})"; |
| |
| return base::StringPrintf(custom_arg_format, test_name, |
| base::ToString(GetEnableIncognito())); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiAuthRequiredTest, |
| WebRequestAuthRequired) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // If running in incognito, create an incognito browser so the test |
| // framework can create an incognito window. |
| const bool incognito = GetEnableIncognito(); |
| if (incognito) { |
| CreateIncognitoBrowser(profile()); |
| } |
| |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required", |
| {.custom_arg = FormatCustomArg("authRequiredNonBlocking").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required", |
| {.custom_arg = FormatCustomArg("authRequiredSyncNoAction").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required", |
| {.custom_arg = FormatCustomArg("authRequiredSyncCancelAuth").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required", |
| {.custom_arg = FormatCustomArg("authRequiredSyncSetAuth").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiAuthRequiredTest, |
| WebRequestAuthRequiredAsync) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // If running in incognito, create an incognito browser so the tests |
| // run in an incognito window. |
| const bool incognito = GetEnableIncognito(); |
| if (incognito) { |
| CreateIncognitoBrowser(profile()); |
| } |
| |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required_async", |
| {.custom_arg = FormatCustomArg("authRequiredAsyncNoAction").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required_async", |
| {.custom_arg = FormatCustomArg("authRequiredAsyncCancelAuth").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest/test_auth_required_async", |
| {.custom_arg = FormatCustomArg("authRequiredAsyncSetAuth").c_str()}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| } |
| |
| // This is flaky on wide variety of platforms (beyond that tracked previously in |
| // https://crbug.com/998369). See https://crbug.com/1026001. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiAuthRequiredTest, |
| DISABLED_WebRequestAuthRequiredParallel) { |
| const bool incognito = GetEnableIncognito(); |
| if (incognito) { |
| CreateIncognitoBrowser(profile()); |
| } |
| |
| const char* const custom_arg = incognito ? R"({"runInIncognito": true})" |
| : R"({"runInIncognito": false})"; |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_auth_required_parallel", |
| {.custom_arg = custom_arg}, |
| {.allow_in_incognito = incognito})) |
| << message_; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ExtensionWebRequestApiAuthRequiredTest, |
| ::testing::Values(ARTestParams(ProfileMode::kUserProfile, |
| ContextType::kPersistentBackground))); |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackgroundIncognito, |
| ExtensionWebRequestApiAuthRequiredTest, |
| ::testing::Values(ARTestParams(ProfileMode::kIncognito, |
| ContextType::kPersistentBackground))); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionWebRequestApiAuthRequiredTest, |
| ::testing::Values(ARTestParams(ProfileMode::kUserProfile, |
| ContextType::kServiceWorkerMV2))); |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorkerIncognito, |
| ExtensionWebRequestApiAuthRequiredTest, |
| ::testing::Values(ARTestParams(ProfileMode::kIncognito, |
| ContextType::kServiceWorkerMV2))); |
| |
| struct AuthRequiredServiceWorkerTestParams { |
| bool under_service_worker_control; |
| ContextType context_type; |
| }; |
| |
| // OnAuthRequired tests for subresource and sub frame, under service worker |
| // control and not under service worker control. |
| class ExtensionWebRequestApiAuthRequiredTestVariousContext |
| : public testing::WithParamInterface<AuthRequiredServiceWorkerTestParams>, |
| public ExtensionApiTest { |
| public: |
| ExtensionWebRequestApiAuthRequiredTestVariousContext() |
| : ExtensionApiTest(GetParam().context_type) {} |
| ~ExtensionWebRequestApiAuthRequiredTestVariousContext() override = default; |
| ExtensionWebRequestApiAuthRequiredTestVariousContext( |
| const ExtensionWebRequestApiAuthRequiredTestVariousContext&) = delete; |
| ExtensionWebRequestApiAuthRequiredTestVariousContext& operator=( |
| const ExtensionWebRequestApiAuthRequiredTestVariousContext&) = delete; |
| |
| void InstallRequestOnAuthRequiredTypeReportingExtension() { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request onAuthRequired Type Reporting Extension", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| |
| // Extension script that will send message about the request type of |
| // onAuthRequired event, and cancel the request. |
| static constexpr char kBackgroundScript[] = R"( |
| console.log('extension running'); |
| chrome.webRequest.onAuthRequired.addListener( |
| function(details, callback) { |
| console.log('onAuthRequired fired for ' + details.type); |
| chrome.test.sendMessage(details.type); |
| return {cancel: true}; |
| }, |
| {urls: ['<all_urls>']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready'); |
| )"; |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundScript); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| void RegisterServiceWorker() { |
| auto* web_contents = GetActiveWebContents(); |
| GURL url = |
| embedded_test_server()->GetURL("/workers/service_worker_setup.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ("ok", EvalJs(web_contents, "setup();")); |
| } |
| |
| void RunCommonTestSetup() { |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| InstallRequestOnAuthRequiredTypeReportingExtension(); |
| |
| // Register a service worker for a variation of the test and not register it |
| // for another variation, so that we can verify that onAuthRequired event is |
| // fired regardless of whether the page is under service worker control. |
| if (GetParam().under_service_worker_control) { |
| RegisterServiceWorker(); |
| } |
| |
| // Navigate to the test page. |
| EXPECT_TRUE( |
| NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/workers/simple.html"))); |
| } |
| |
| // Ensures auth required event is received for subresource fetch. |
| void RunAuthRequiredTestForSubResource() { |
| RunCommonTestSetup(); |
| |
| // Make a fetch from the test page for a resource that requires auth. |
| ExtensionTestMessageListener listener("xmlhttprequest"); |
| static constexpr char kSubResourceUrl[] = |
| "/auth-basic/auth_required_subresource?realm=auth_required_subresource"; |
| std::string fetch_url = |
| embedded_test_server()->GetURL(kSubResourceUrl).spec(); |
| EXPECT_EQ(401, EvalJs(GetActiveWebContents(), |
| "try_fetch_status('" + fetch_url + "');")); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Ensures auth required event is received for sub frame navigation. |
| void RunAuthRequiredTestForSubFrame() { |
| RunCommonTestSetup(); |
| |
| // Add an iframe for a source that requires auth. |
| ExtensionTestMessageListener listener("sub_frame"); |
| static constexpr char kSubFrameUrl[] = |
| "/auth-basic/auth_required_subframe?realm=auth_required_subframe"; |
| std::string frame_url = embedded_test_server()->GetURL(kSubFrameUrl).spec(); |
| static constexpr char kAddIframeScript[] = R"( |
| const el = document.createElement('iframe'); |
| el.src = $1; |
| document.body.appendChild(el); |
| )"; |
| content::EvalJsResult result = |
| EvalJs(GetActiveWebContents(), |
| content::JsReplace(kAddIframeScript, frame_url)); |
| ASSERT_THAT(result, content::EvalJsResult::IsOk()); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(PersistentBackgroundWithServiceWorker, |
| ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| ::testing::Values(AuthRequiredServiceWorkerTestParams( |
| true, |
| ContextType::kPersistentBackground))); |
| INSTANTIATE_TEST_SUITE_P(PersistentBackgroundWithoutServiceWorker, |
| ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| ::testing::Values(AuthRequiredServiceWorkerTestParams( |
| false, |
| ContextType::kPersistentBackground))); |
| |
| INSTANTIATE_TEST_SUITE_P(ServiceWorkerExtensionWithServiceWorker, |
| ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| ::testing::Values(AuthRequiredServiceWorkerTestParams( |
| true, |
| ContextType::kServiceWorkerMV2))); |
| INSTANTIATE_TEST_SUITE_P(ServiceWorkerExtensionWithoutServiceWorker, |
| ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| ::testing::Values(AuthRequiredServiceWorkerTestParams( |
| false, |
| ContextType::kServiceWorkerMV2))); |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| SubFrame) { |
| RunAuthRequiredTestForSubFrame(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiAuthRequiredTestVariousContext, |
| SubResource) { |
| RunAuthRequiredTestForSubResource(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestBlocking) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_blocking", |
| {.custom_arg = R"({"testSuite": "normal"})"})) |
| << message_; |
| } |
| |
| // This test times out regularly on win_rel trybots. See http://crbug.com/122178 |
| // Also on Linux/ChromiumOS debug, ASAN and MSAN builds. |
| // https://crbug.com/670415 |
| // Slower and flaky tests should be isolated in the "slow" group of tests in |
| // the JS file. This prevents losing test coverage for those tests that are |
| // not causing timeouts and flakes. |
| // TODO(crbug.com/40916455): Investigate the flakiness across all |
| // platforms and re-enable. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestBlockingSlow) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_blocking", |
| {.custom_arg = R"({"testSuite": "slow"})"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestBlockingSetCookieHeader) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_blocking_cookie")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestExtraHeaders) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_extra_headers")) << message_; |
| } |
| |
| // Flaky on all platforms: https://crbug.com/1003661 |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestExtraHeaders_Auth) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_extra_headers_auth")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestChangeCSPHeaders) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_change_csp_headers")) |
| << message_; |
| } |
| |
| // TODO: crbug.com/1450976 - Re-enable tests on Mac and CrOS. |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS) |
| #define MAYBE_WebRequestCORSWithExtraHeaders \ |
| DISABLED_WebRequestCORSWithExtraHeaders |
| #else |
| #define MAYBE_WebRequestCORSWithExtraHeaders WebRequestCORSWithExtraHeaders |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_WebRequestCORSWithExtraHeaders) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_cors")) << message_; |
| } |
| |
| #if defined(ADDRESS_SANITIZER) |
| #define MAYBE_WebRequestRedirects DISABLED_WebRequestRedirects |
| #else |
| #define MAYBE_WebRequestRedirects WebRequestRedirects |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_WebRequestRedirects) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_redirects")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestRedirectsWithExtraHeaders) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_redirects", |
| {.custom_arg = R"({"useExtraHeaders": true})"})) |
| << message_; |
| } |
| |
| // Tests that redirects from secure to insecure don't send the referrer header. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestRedirectsToInsecure) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL insecure_destination = |
| embedded_test_server()->GetURL("/extensions/test_file.html"); |
| net::EmbeddedTestServer https_test_server( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| https_test_server.ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(https_test_server.Start()); |
| |
| GURL url = https_test_server.GetURL("/webrequest/simulate_click.html"); |
| |
| base::Value::List custom_args; |
| custom_args.Append(url.spec()); |
| custom_args.Append(insecure_destination.spec()); |
| |
| std::string config_string = base::WriteJson(custom_args).value_or(""); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_redirects_from_secure", |
| {.custom_arg = config_string.c_str()})) |
| << message_; |
| } |
| |
| // Tests redirects around workers. To test service workers, the HTTPS test |
| // server is used. |
| // TODO(crbug.com/40255652): test is flaky on linux-chromeos-rel. |
| // TODO(crbug.com/40259518): test is flaky on Mac10.14. |
| // TODO(crbug.com/40282182): test is flaky on linux tests. |
| // TODO(crbug.com/393555373): test is flaky on Windows. |
| #if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \ |
| BUILDFLAG(IS_WIN) |
| #define MAYBE_WebRequestRedirectsWorkers DISABLED_WebRequestRedirectsWorkers |
| #else |
| #define MAYBE_WebRequestRedirectsWorkers WebRequestRedirectsWorkers |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_WebRequestRedirectsWorkers) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| net::EmbeddedTestServer https_test_server( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| https_test_server.ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(https_test_server.Start()); |
| |
| GURL base_url = |
| https_test_server.GetURL("/webrequest/test_redirects_workers/page/"); |
| base::Value::Dict custom_args; |
| custom_args.Set("base_url", base_url.spec()); |
| std::string config_string = base::WriteJson(custom_args).value_or(""); |
| |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_redirects_workers", |
| {.custom_arg = config_string.c_str()})) |
| << message_; |
| } |
| |
| // TODO(crbug.com/40916455): test is flaky on multiple platforms. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestSubresourceRedirects) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_subresource_redirects")) |
| << message_; |
| } |
| |
| // TODO(crbug.com/40916455): test is flaky on multiple platforms. |
| IN_PROC_BROWSER_TEST_P( |
| ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestSubresourceRedirectsWithExtraHeaders) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_subresource_redirects", |
| {.custom_arg = R"({"useExtraHeaders": true})"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestNewTab) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_new_tab")) << message_; |
| |
| WebContents* tab = GetActiveWebContents(); |
| EXPECT_TRUE(content::WaitForLoadStop(tab)); |
| |
| ResultCatcher catcher; |
| |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile()); |
| const Extension* extension = |
| registry->enabled_extensions().GetByID(last_loaded_extension_id()); |
| GURL url = extension->GetResourceURL("newTab/a.html"); |
| |
| ASSERT_TRUE(NavigateToURL(tab, url)); |
| |
| // There's a link on a.html with target=_blank. Click on it to open it in a |
| // new tab. |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::Type::kMouseDown, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebMouseEvent::Button::kLeft; |
| mouse_event.SetPositionInWidget(7, 7); |
| mouse_event.click_count = 1; |
| tab->GetPrimaryMainFrame() |
| ->GetRenderViewHost() |
| ->GetWidget() |
| ->ForwardMouseEvent(mouse_event); |
| mouse_event.SetType(blink::WebInputEvent::Type::kMouseUp); |
| tab->GetPrimaryMainFrame() |
| ->GetRenderViewHost() |
| ->GetWidget() |
| ->ForwardMouseEvent(mouse_event); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarative1) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_declarative", |
| {.custom_arg = R"({"testSuite": "normal1"})"})) |
| << message_; |
| } |
| |
| // This test fixture runs all of the broken and flaky tests. It's disabled |
| // until these tests are fixed and moved to the set of tests that aren't |
| // broken or flaky. Should tests become flaky, they can be moved here. |
| // See https://crbug.com/846555. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_WebRequestDeclarative1Broken) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_declarative", |
| {.custom_arg = R"({"testSuite": "broken"})"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarative2) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest/test_declarative", |
| {.custom_arg = R"({"testSuite": "normal2"})"})) |
| << message_; |
| } |
| |
| void ExtensionWebRequestApiTest::RunPermissionTest( |
| const char* extension_directory, |
| bool load_extension_with_incognito_permission, |
| bool wait_for_extension_loaded_in_incognito, |
| const char* expected_content_regular_window, |
| const char* exptected_content_incognito_window, |
| ContextType context_type) { |
| ResultCatcher catcher; |
| catcher.RestrictToBrowserContext(profile()); |
| ResultCatcher catcher_incognito; |
| catcher_incognito.RestrictToBrowserContext( |
| profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true)); |
| |
| ExtensionTestMessageListener listener("done"); |
| ExtensionTestMessageListener listener_incognito("done_incognito"); |
| |
| ASSERT_TRUE(LoadExtension( |
| test_data_dir_.AppendASCII("webrequest_permissions") |
| .AppendASCII(extension_directory), |
| {.allow_in_incognito = load_extension_with_incognito_permission, |
| .context_type = context_type})); |
| |
| // Test that navigation in regular window is properly redirected. |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // This navigation should be redirected. |
| WebContents* tab = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| tab, embedded_test_server()->GetURL("/extensions/test_file.html"))); |
| |
| EXPECT_EQ(expected_content_regular_window, |
| content::EvalJs(tab, "document.body.textContent")); |
| |
| // Test that navigation in OTR window is properly redirected. |
| Browser* otr_browser = OpenURLOffTheRecord(profile(), GURL("about:blank")); |
| |
| if (wait_for_extension_loaded_in_incognito) { |
| EXPECT_TRUE(listener_incognito.WaitUntilSatisfied()); |
| } |
| |
| // This navigation should be redirected if |
| // load_extension_with_incognito_permission is true. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| otr_browser, |
| embedded_test_server()->GetURL("/extensions/test_file.html"))); |
| |
| WebContents* otr_tab = otr_browser->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ(exptected_content_incognito_window, |
| content::EvalJs(otr_tab, "document.body.textContent")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarativePermissionSpanning1) { |
| // Test spanning with incognito permission. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| RunPermissionTest("spanning", true, false, "redirected1", "redirected1", |
| GetContextType()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarativePermissionSpanning2) { |
| // Test spanning without incognito permission. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| RunPermissionTest("spanning", false, false, "redirected1", "", |
| GetContextType()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarativePermissionSplit1) { |
| // Test split with incognito permission. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| RunPermissionTest("split", true, true, "redirected1", "redirected2", |
| GetContextType()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDeclarativePermissionSplit2) { |
| // Test split without incognito permission. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| RunPermissionTest("split", false, false, "redirected1", "", GetContextType()); |
| } |
| |
| // TODO(crbug.com/41010858): Cure these flaky tests. |
| // TODO(crbug.com/40734863): Bulk-disabled as part of mac arm64 bot greening |
| // TODO(crbug.com/40773828): Further disabled due to ongoing flakiness. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, DISABLED_PostData1) { |
| // Test HTML form POST data access with the default and "url" encoding. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_post1.html"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, DISABLED_PostData2) { |
| // Test HTML form POST data access with the multipart and plaintext encoding. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_post2.html"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DeclarativeSendMessage) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest_sendmessage")) << message_; |
| } |
| |
| // Check that reloading an extension that runs in incognito split mode and |
| // has two active background pages with registered events does not crash the |
| // browser. Regression test for http://crbug.com/40309927 |
| // TODO(crbug.com/40897394): Flaky on Linux. |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_IncognitoSplitModeReload DISABLED_IncognitoSplitModeReload |
| #else |
| #define MAYBE_IncognitoSplitModeReload IncognitoSplitModeReload |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_IncognitoSplitModeReload) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| // Wait for rules to be set up. |
| ExtensionTestMessageListener listener("done"); |
| ExtensionTestMessageListener listener_incognito("done_incognito"); |
| |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_reload"), |
| {.allow_in_incognito = true}); |
| ASSERT_TRUE(extension); |
| OpenURLOffTheRecord(profile(), GURL("about:blank")); |
| |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| EXPECT_TRUE(listener_incognito.WaitUntilSatisfied()); |
| |
| // Reload extension and wait for rules to be set up again. This should not |
| // crash the browser. |
| ExtensionTestMessageListener listener2("done"); |
| ExtensionTestMessageListener listener_incognito2("done_incognito"); |
| |
| ReloadExtension(extension->id()); |
| |
| EXPECT_TRUE(listener2.WaitUntilSatisfied()); |
| EXPECT_TRUE(listener_incognito2.WaitUntilSatisfied()); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| ExtensionRequests) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ExtensionTestMessageListener listener_main1("web_request_status1", |
| ReplyBehavior::kWillReply); |
| ExtensionTestMessageListener listener_main2("web_request_status2", |
| ReplyBehavior::kWillReply); |
| // Android does not support platform apps, so tests for requests from apps are |
| // omitted. See https://crbug.com/440452765 for more details. |
| #if !BUILDFLAG(IS_ANDROID) |
| ExtensionTestMessageListener listener_app("app_done"); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| ExtensionTestMessageListener listener_extension("extension_done"); |
| |
| // Set up webRequest listener |
| ASSERT_TRUE( |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_extensions/main"))); |
| #if !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(listener_main1.WaitUntilSatisfied()); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(listener_main2.WaitUntilSatisfied()); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Perform some network activity in an app. |
| ASSERT_TRUE( |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_extensions/app"), |
| {.context_type = ContextType::kFromManifest})); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| // Perform some network activity in another extension. |
| ASSERT_TRUE(LoadExtension( |
| test_data_dir_.AppendASCII("webrequest_extensions/extension"), |
| {.context_type = ContextType::kFromManifest})); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(listener_app.WaitUntilSatisfied()); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(listener_extension.WaitUntilSatisfied()); |
| |
| // Load a page, a content script from "webrequest_extensions/extension" will |
| // ping us when it is ready. |
| ExtensionTestMessageListener listener_pageready("contentscript_ready", |
| ReplyBehavior::kWillReply); |
| ASSERT_TRUE( |
| NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL( |
| "/extensions/test_file.html?match_webrequest_test"))); |
| EXPECT_TRUE(listener_pageready.WaitUntilSatisfied()); |
| |
| // The extension and app-generated requests should not have triggered any |
| // webRequest event filtered by type 'xmlhttprequest'. |
| // (check this here instead of before the navigation, in case the webRequest |
| // event routing is slow for some reason). |
| ExtensionTestMessageListener listener_result; |
| listener_main1.Reply(""); |
| EXPECT_TRUE(listener_result.WaitUntilSatisfied()); |
| EXPECT_EQ("Did not intercept any requests.", listener_result.message()); |
| |
| ExtensionTestMessageListener listener_contentscript("contentscript_done"); |
| ExtensionTestMessageListener listener_framescript("framescript_done"); |
| |
| // Proceed with the final tests: Let the content script fire a request and |
| // then load an iframe which also fires a XHR request. |
| listener_pageready.Reply(""); |
| EXPECT_TRUE(listener_contentscript.WaitUntilSatisfied()); |
| EXPECT_TRUE(listener_framescript.WaitUntilSatisfied()); |
| |
| // Collect the visited URLs. The content script and subframe does not run in |
| // the extension's process, so the requests should be visible to the main |
| // extension. |
| listener_result.Reset(); |
| listener_main2.Reply(""); |
| EXPECT_TRUE(listener_result.WaitUntilSatisfied()); |
| |
| // The extension frame does run in the extension's process. Any requests made |
| // by it should not be visible to other extensions, since they won't have |
| // access to the request initiator. |
| // |
| // OTOH, the content script executes fetches/XHRs as-if they were initiated by |
| // the webpage that the content script got injected into. Here, the webpage |
| // has origin of http://127.0.0.1:<some port>, and so the webRequest API |
| // extension should have access to the request. |
| EXPECT_EQ("Intercepted requests: ?contentscript", listener_result.message()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, HostedAppRequest) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GURL hosted_app_url(embedded_test_server()->GetURL( |
| "/extensions/api_test/webrequest_hosted_app/index.html")); |
| scoped_refptr<const Extension> hosted_app = |
| ExtensionBuilder() |
| .SetManifest( |
| base::Value::Dict() |
| .Set("name", "Some hosted app") |
| .Set("version", "1") |
| .Set("manifest_version", 2) |
| .Set("app", |
| base::Value::Dict().Set( |
| "launch", base::Value::Dict().Set( |
| "web_url", hosted_app_url.spec())))) |
| .Build(); |
| extension_registrar()->AddExtension(hosted_app); |
| |
| ExtensionTestMessageListener listener1("main_frame"); |
| ExtensionTestMessageListener listener2("xmlhttprequest"); |
| |
| ASSERT_TRUE( |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_hosted_app"))); |
| |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), hosted_app_url)); |
| |
| EXPECT_TRUE(listener1.WaitUntilSatisfied()); |
| EXPECT_TRUE(listener2.WaitUntilSatisfied()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests that WebRequest works with runtime host permissions. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestWithWithheldPermissions) { |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it's initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| ScriptingPermissionsModifier(profile(), base::WrapRefCounted(extension)) |
| .SetWithholdHostPermissions(true); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Navigate the browser to a page in a new tab. The page at "a.com" has two |
| // two cross-origin iframes to "b.com" and "c.com". |
| const std::string kHost = "a.com"; |
| GURL url = embedded_test_server()->GetURL(kHost, "/iframe_cross_site.html"); |
| NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK); |
| params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| ui_test_utils::NavigateToURL(¶ms); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| ExtensionActionRunner* runner = |
| ExtensionActionRunner::GetForWebContents(web_contents); |
| ASSERT_TRUE(runner); |
| |
| int port = embedded_test_server()->port(); |
| const std::string kXhrPath = "simple.html"; |
| |
| // The extension shouldn't have currently received any webRequest events, |
| // since it doesn't have any permissions. |
| { |
| EXPECT_EQ(0, GetWebRequestCountFromBackgroundScript(extension, profile())); |
| |
| content::RenderFrameHostWrapper main_frame( |
| web_contents->GetPrimaryMainFrame()); |
| content::RenderFrameHostWrapper child_frame( |
| ChildFrameAt(main_frame.get(), 0)); |
| ASSERT_TRUE(child_frame); |
| const std::string kChildHost = child_frame->GetLastCommittedURL().GetHost(); |
| |
| // The extension shouldn't be able to intercept the xhr requests since it |
| // doesn't have any permissions. |
| PerformXhrInFrame(main_frame.get(), kHost, port, kXhrPath); |
| PerformXhrInFrame(child_frame.get(), kChildHost, port, kXhrPath); |
| EXPECT_EQ(0, GetWebRequestCountFromBackgroundScript(extension, profile())); |
| EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, |
| runner->GetBlockedActions(extension->id())); |
| |
| // Grant activeTab permission. |
| auto reload_page_dialog_reset = |
| ReloadPageDialogController::AcceptDialogForTesting(true); |
| runner->RunAction(extension, true); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| } |
| |
| // The runner will have refreshed the page, and the extension will have |
| // received access to the main-frame ("a.com"). It should still not be able to |
| // intercept the cross-origin sub-frame requests to "b.com" and "c.com". |
| content::RenderFrameHostWrapper main_frame( |
| web_contents->GetPrimaryMainFrame()); |
| content::RenderFrameHostWrapper child_frame( |
| ChildFrameAt(main_frame.get(), 0)); |
| const std::string kChildHost = child_frame->GetLastCommittedURL().GetHost(); |
| |
| ASSERT_TRUE(child_frame); |
| EXPECT_TRUE( |
| HasSeenWebRequestInBackgroundScript(extension, profile(), "a.com")); |
| EXPECT_FALSE( |
| HasSeenWebRequestInBackgroundScript(extension, profile(), "b.com")); |
| EXPECT_FALSE( |
| HasSeenWebRequestInBackgroundScript(extension, profile(), "c.com")); |
| |
| // The withheld sub-frame requests should not show up as a blocked action. |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| |
| int request_count = |
| GetWebRequestCountFromBackgroundScript(extension, profile()); |
| |
| // ... and the extension should receive future events. |
| PerformXhrInFrame(main_frame.get(), kHost, port, kXhrPath); |
| ++request_count; |
| EXPECT_EQ(request_count, |
| GetWebRequestCountFromBackgroundScript(extension, profile())); |
| |
| // However, activeTab only grants access to the main frame, not to child |
| // frames. As such, trying to XHR in the child frame should still fail. |
| PerformXhrInFrame(child_frame.get(), kChildHost, port, kXhrPath); |
| EXPECT_EQ(request_count, |
| GetWebRequestCountFromBackgroundScript(extension, profile())); |
| // But since there's no way for the user to currently grant access to child |
| // frames, this shouldn't show up as a blocked action. |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| |
| // Revoke the extension's tab permissions. |
| ActiveTabPermissionGranter* granter = |
| ActiveTabPermissionGranter::FromWebContents(web_contents); |
| ASSERT_TRUE(granter); |
| granter->RevokeForTesting(); |
| base::RunLoop().RunUntilIdle(); |
| |
| // The extension should no longer receive webRequest events since they are |
| // withheld. The extension icon should get updated to show the wants-to-run |
| // badge UI. |
| TestExtensionActionDispatcherObserver action_updated_waiter(profile(), |
| extension->id()); |
| PerformXhrInFrame(main_frame.get(), kHost, port, kXhrPath); |
| action_updated_waiter.Wait(); |
| EXPECT_EQ(web_contents, action_updated_waiter.last_web_contents()); |
| |
| EXPECT_EQ(request_count, |
| GetWebRequestCountFromBackgroundScript(extension, profile())); |
| EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, |
| runner->GetBlockedActions(extension->id())); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Test that extensions with granted runtime host permissions to a tab can |
| // intercept cross-origin requests from that tab. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestWithheldPermissionsCrossOriginRequests) { |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it's initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| ScriptingPermissionsModifier(profile(), base::WrapRefCounted(extension)) |
| .SetWithholdHostPermissions(true); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL( |
| "a.com", "/extensions/cross_site_script.html"))); |
| |
| const std::string kCrossSiteHost("b.com"); |
| EXPECT_FALSE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| kCrossSiteHost)); |
| |
| ExtensionActionRunner* runner = |
| ExtensionActionRunner::GetForWebContents(web_contents); |
| ASSERT_TRUE(runner); |
| |
| EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, |
| runner->GetBlockedActions(extension->id())); |
| |
| // Grant runtime host permission to the page. The page should refresh. Even |
| // though the request is for b.com (and the extension only has access to |
| // a.com), it should still see the request. This is necessary for extensions |
| // with webRequest to work with runtime host permissions. |
| // https://crbug.com/851722. |
| auto reload_page_dialog_reset = |
| ReloadPageDialogController::AcceptDialogForTesting(true); |
| runner->RunAction(extension, true /* grant tab permissions */); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| |
| EXPECT_TRUE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| kCrossSiteHost)); |
| } |
| |
| // Tests behavior when an extension has withheld access to a request's URL, but |
| // not the initiator's (tab's) URL. Regression test for |
| // https://crbug.com/891586. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WithheldHostPermissionsForCrossOriginWithoutInitiator) { |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // TODO(devlin): This is essentially copied from the webrequest_activetab |
| // API test extension, but has different permissions. Maybe it's worth having |
| // all tests use a common pattern? |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Web Request Withheld Hosts", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["*://b.com:*/*", "webRequest"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| R"(self.webRequestCount = 0; |
| self.requestedHostnames = []; |
| |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| ++self.webRequestCount; |
| self.requestedHostnames.push((new URL(details.url)).hostname); |
| }, {urls:['<all_urls>']}); |
| chrome.test.sendMessage('ready');)"); |
| |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it's initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension) << message_; |
| ScriptingPermissionsModifier(profile(), base::WrapRefCounted(extension)) |
| .SetWithholdHostPermissions(true); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Navigate to example.com, which has a cross-site script to b.com. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL( |
| "example.com", "/extensions/cross_site_script.html"))); |
| |
| ExtensionActionRunner* runner = |
| ExtensionActionRunner::GetForWebContents(web_contents); |
| ASSERT_TRUE(runner); |
| |
| // Even though the extension has access to b.com, it shouldn't show that it |
| // wants to run, because example.com is not a requested host. |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| EXPECT_FALSE( |
| HasSeenWebRequestInBackgroundScript(extension, profile(), "b.com")); |
| |
| // Navigating to b.com (so that the script is hosted on the same origin as |
| // the WebContents) should show the extension wants to run. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL( |
| "b.com", "/extensions/cross_site_script.html"))); |
| EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST, |
| runner->GetBlockedActions(extension->id())); |
| } |
| |
| // Verify that requests to clientsX.google.com are protected properly. |
| // First test requests from a standard renderer and then a request from the |
| // browser process. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextTypeMV3, |
| WebRequestClientsGoogleComProtection) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it's initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("webrequest_clients_google_com")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| auto get_clients_google_request_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), |
| "self.clientsGoogleWebRequestCount"); |
| }; |
| auto get_yahoo_request_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), |
| "self.yahooWebRequestCount"); |
| }; |
| |
| EXPECT_EQ(0, get_clients_google_request_count()); |
| EXPECT_EQ(0, get_yahoo_request_count()); |
| |
| auto* web_contents = GetActiveWebContents(); |
| GURL main_frame_url = |
| embedded_test_server()->GetURL("www.example.com", "/simple.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, main_frame_url)); |
| content::WaitForLoadStop(web_contents); |
| |
| EXPECT_EQ(0, get_clients_google_request_count()); |
| EXPECT_EQ(0, get_yahoo_request_count()); |
| |
| // Attempt to issue a request to clients1.google.com from the renderer. This |
| // will fail, but should still be visible to the WebRequest API. |
| const char kRequest[] = R"( |
| var xhr = new XMLHttpRequest(); |
| xhr.open('GET', 'http://clients1.google.com'); |
| new Promise(resolve => { |
| xhr.onload = () => {resolve(true);}; |
| xhr.onerror = () => {resolve(false);}; |
| xhr.send(); |
| }); |
| )"; |
| EXPECT_EQ(false, EvalJs(web_contents->GetPrimaryMainFrame(), kRequest)); |
| // Requests always fail due to cross origin nature. |
| |
| EXPECT_EQ(1, get_clients_google_request_count()); |
| EXPECT_EQ(0, get_yahoo_request_count()); |
| |
| auto make_browser_request = [this](const GURL& url) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = url; |
| request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| request->destination = network::mojom::RequestDestination::kEmpty; |
| request->resource_type = |
| static_cast<int>(blink::mojom::ResourceType::kSubResource); |
| |
| auto* url_loader_factory = profile() |
| ->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess() |
| .get(); |
| content::SimpleURLLoaderTestHelper loader_helper; |
| auto loader = network::SimpleURLLoader::Create( |
| std::move(request), TRAFFIC_ANNOTATION_FOR_TESTS); |
| loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory, loader_helper.GetCallbackDeprecated()); |
| |
| // Wait for the response to complete. |
| loader_helper.WaitForCallback(); |
| EXPECT_TRUE(loader_helper.response_body()); |
| EXPECT_EQ(200, loader->ResponseInfo()->headers->response_code()); |
| }; |
| |
| // Now perform a request to "client1.google.com" from the browser process. |
| // This should *not* be visible to the WebRequest API. We should still have |
| // only seen the single render-initiated request from the first half of the |
| // test. |
| make_browser_request( |
| embedded_test_server()->GetURL("clients1.google.com", "/simple.html")); |
| EXPECT_EQ(1, get_clients_google_request_count()); |
| |
| // Other non-navigation browser requests should also be hidden from |
| // extensions. |
| make_browser_request( |
| embedded_test_server()->GetURL("yahoo.com", "/simple.html")); |
| EXPECT_EQ(0, get_yahoo_request_count()); |
| } |
| |
| // Verify that requests for PAC scripts are protected properly. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextTypeMV3, |
| WebRequestPacRequestProtection) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it's initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_pac_request")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Configure a PAC script. Need to do this after the extension is loaded, so |
| // that the PAC isn't already loaded by the time the extension starts |
| // affecting requests. |
| PrefService* pref_service = profile()->GetPrefs(); |
| pref_service->SetDict(proxy_config::prefs::kProxy, |
| ProxyConfigDictionary::CreatePacScript( |
| embedded_test_server()->GetURL("/self.pac").spec(), |
| true /* pac_mandatory */)); |
| // Flush the proxy configuration change over the Mojo pipe to avoid any races. |
| ProfileNetworkContextServiceFactory::GetForContext(profile()) |
| ->FlushProxyConfigMonitorForTesting(); |
| |
| // Navigate to a page. The URL doesn't matter. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| GURL("http://does.not.resolve.test/title2.html"))); |
| |
| // The extension should not have seen the PAC request. |
| EXPECT_EQ(0, GetCountFromBackgroundScript(extension, profile(), |
| "self.pacRequestCount")); |
| |
| // The extension should have seen the request for the main frame. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.title2RequestCount")); |
| |
| // The PAC request should have succeeded, as should the subsequent URL |
| // request. |
| EXPECT_EQ( |
| content::PAGE_TYPE_NORMAL, |
| web_contents->GetController().GetLastCommittedEntry()->GetPageType()); |
| } |
| |
| // Checks that the Dice response header is protected for Gaia URLs, but not |
| // other URLs. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestDiceHeaderProtection) { |
| // Load an extension that registers a listener for webRequest events, and |
| // wait until it is initialized. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_dice_header")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Setup a web contents observer to inspect the response headers after the |
| // extension was run. |
| class TestWebContentsObserver : public content::WebContentsObserver { |
| public: |
| explicit TestWebContentsObserver(content::WebContents* contents) |
| : WebContentsObserver(contents) {} |
| |
| void DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| // Check that the extension cannot add a Dice header. |
| const net::HttpResponseHeaders* headers = |
| navigation_handle->GetResponseHeaders(); |
| std::optional<std::string> dice_header_value = |
| headers->GetNormalizedHeader("X-Chrome-ID-Consistency-Response"); |
| EXPECT_TRUE(dice_header_value); |
| dice_header_value_ = dice_header_value.value_or(std::string()); |
| std::optional<std::string> new_header_value = |
| headers->GetNormalizedHeader("X-New-Header"); |
| EXPECT_TRUE(new_header_value); |
| new_header_value_ = new_header_value.value_or(std::string()); |
| std::optional<std::string> control_header_value = |
| headers->GetNormalizedHeader("X-Control"); |
| EXPECT_TRUE(control_header_value); |
| control_header_value_ = control_header_value.value_or(std::string()); |
| |
| did_finish_navigation_called_ = true; |
| } |
| |
| bool did_finish_navigation_called() const { |
| return did_finish_navigation_called_; |
| } |
| |
| const std::string& dice_header_value() const { return dice_header_value_; } |
| |
| const std::string& new_header_value() const { return new_header_value_; } |
| |
| const std::string& control_header_value() const { |
| return control_header_value_; |
| } |
| |
| void Clear() { |
| did_finish_navigation_called_ = false; |
| dice_header_value_.clear(); |
| new_header_value_.clear(); |
| control_header_value_.clear(); |
| } |
| |
| private: |
| bool did_finish_navigation_called_ = false; |
| std::string dice_header_value_; |
| std::string new_header_value_; |
| std::string control_header_value_; |
| }; |
| |
| auto* web_contents = GetActiveWebContents(); |
| TestWebContentsObserver test_webcontents_observer(web_contents); |
| |
| // Navigate to the Gaia URL intercepted by the extension. |
| GURL url = |
| embedded_test_server()->GetURL("gaia.com", "/extensions/dice.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Check that the Dice header was not changed by the extension. |
| EXPECT_TRUE(test_webcontents_observer.did_finish_navigation_called()); |
| EXPECT_EQ(kHeaderValueFromServer, |
| test_webcontents_observer.dice_header_value()); |
| EXPECT_EQ(kHeaderValueFromExtension, |
| test_webcontents_observer.new_header_value()); |
| EXPECT_EQ(kHeaderValueFromExtension, |
| test_webcontents_observer.control_header_value()); |
| |
| // Check that the Dice header cannot be read by the extension. |
| EXPECT_EQ(0, GetCountFromBackgroundScript(extension, profile(), |
| "self.diceResponseHeaderCount")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.controlResponseHeaderCount")); |
| |
| // Navigate to a non-Gaia URL intercepted by the extension. |
| test_webcontents_observer.Clear(); |
| url = embedded_test_server()->GetURL("example.com", "/extensions/dice.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Check that the Dice header was changed by the extension. |
| EXPECT_TRUE(test_webcontents_observer.did_finish_navigation_called()); |
| EXPECT_EQ(kHeaderValueFromExtension, |
| test_webcontents_observer.dice_header_value()); |
| EXPECT_EQ(kHeaderValueFromExtension, |
| test_webcontents_observer.new_header_value()); |
| EXPECT_EQ(kHeaderValueFromExtension, |
| test_webcontents_observer.control_header_value()); |
| |
| // Check that the Dice header can be read by the extension. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.diceResponseHeaderCount")); |
| EXPECT_EQ(2, GetCountFromBackgroundScript(extension, profile(), |
| "self.controlResponseHeaderCount")); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Test that the webRequest events are dispatched for the WebSocket handshake |
| // requests. |
| // TODO(crbug.com/40715657): Test is flaky on multiple platforms. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, DISABLED_WebSocketRequest) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(StartWebSocketServer()); |
| ASSERT_TRUE( |
| RunExtensionTest("webrequest", {.extension_url = "test_websocket.html"})) |
| << message_; |
| } |
| |
| // Test that the webRequest events are dispatched for the WebSocket handshake |
| // requests when authenrication is requested by server. |
| // TODO(crbug.com/40168662) Re-enable test |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| DISABLED_WebSocketRequestAuthRequired) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(StartWebSocketServer(/*enable_basic_auth=*/true)); |
| ASSERT_TRUE(RunExtensionTest("webrequest", |
| {.extension_url = "test_websocket_auth.html"})) |
| << message_; |
| } |
| |
| // Test that the webRequest events are dispatched for the WebSocket handshake |
| // requests. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, WebSocketRequestOnWorker) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(StartWebSocketServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest", |
| {.extension_url = "test_websocket_worker.html"})) |
| << message_; |
| } |
| |
| // Tests that a clean close from the server is not reported as an error when |
| // there is a race between OnDropChannel and SendFrame. |
| // Regression test for https://crbug.com/937790. |
| // |
| // TODO(b:332825952): Flaky on linux-chromeos-dbg |
| #if BUILDFLAG(IS_CHROMEOS) |
| #define MAYBE_WebSocketCleanClose DISABLED_WebSocketCleanClose |
| #else |
| #define MAYBE_WebSocketCleanClose WebSocketCleanClose |
| #endif |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, MAYBE_WebSocketCleanClose) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(StartWebSocketServer()); |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest", {.extension_url = "test_websocket_clean_close.html"})) |
| << message_; |
| } |
| |
| // Depends on declarativeWebRequest. crbug.com/332512510. |
| class ExtensionWebRequestApiWebTransportTest |
| : public ExtensionWebRequestApiTest { |
| public: |
| ExtensionWebRequestApiWebTransportTest() { server_.Start(); } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionWebRequestApiTest::SetUpCommandLine(command_line); |
| server_.SetUpCommandLine(command_line); |
| } |
| |
| void SetUpOnMainThread() override { |
| ExtensionWebRequestApiTest::SetUpOnMainThread(); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| GetTestConfig()->Set("testWebTransportPort", |
| server_.server_address().port()); |
| } |
| |
| protected: |
| bool RunTest(const char* page_url) { |
| return RunExtensionTest("webrequest", {.extension_url = page_url}); |
| } |
| |
| content::WebTransportSimpleTestServer server_; |
| }; |
| |
| // Test that the webRequest events are dispatched for the WebTransport |
| // handshake. |
| // TODO(crbug.com/326122304): Re-enable this test |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiWebTransportTest, DISABLED_Main) { |
| ASSERT_TRUE(RunTest("test_webtransport.html")) << message_; |
| } |
| |
| // Test that the webRequest events are dispatched for the WebTransport |
| // handshake in a dedicated worker. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiWebTransportTest, |
| DedicatedWorker) { |
| ASSERT_TRUE(RunTest("test_webtransport_dedicated_worker.html")) << message_; |
| } |
| |
| // Test that the webRequest events are dispatched for the WebTransport |
| // handshake in a shared worker. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiWebTransportTest, SharedWorker) { |
| ASSERT_TRUE(RunTest("test_webtransport_shared_worker.html")) << message_; |
| } |
| |
| // Test that the webRequest events are dispatched for the WebTransport |
| // handshake in a service worker. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiWebTransportTest, ServiceWorker) { |
| ASSERT_TRUE(RunTest("test_webtransport_service_worker.html")) << message_; |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Test behavior when intercepting requests from a browser-initiated url fetch. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| WebRequestURLLoaderInterception) { |
| // Create an extension that intercepts (and blocks) requests to example.com. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "web_request_browser_interception", |
| "description": "tests that browser requests aren't intercepted", |
| "version": "0.1", |
| "permissions": ["webRequest", "webRequestBlocking", "*://*/*"], |
| "manifest_version": 2, |
| "background": { "scripts": ["background.js"], "persistent": true } |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| function(details) { |
| return {cancel: details.url.indexOf('example.com') != -1}; |
| }, |
| {urls: ["<all_urls>"]}, |
| ["blocking"]); |
| chrome.test.sendMessage('ready');)"); |
| |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Taken from test/data/extensions/body1.html. |
| const char kGoogleBodyContent[] = "dog"; |
| const char kGoogleFullContent[] = "<html>\n<body>dog</body>\n</html>\n"; |
| |
| // Taken from test/data/extensions/body2.html. |
| const char kExampleBodyContent[] = "cat"; |
| const char kExampleFullContent[] = "<html>\n<body>cat</body>\n</html>\n"; |
| |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| std::unique_ptr<net::test_server::BasicHttpResponse> response( |
| new net::test_server::BasicHttpResponse); |
| if (request.relative_url == "/extensions/body1.html") { |
| response->set_code(net::HTTP_OK); |
| response->set_content(kGoogleFullContent); |
| return std::move(response); |
| } else if (request.relative_url == "/extensions/body2.html") { |
| response->set_code(net::HTTP_OK); |
| response->set_content(kExampleFullContent); |
| return std::move(response); |
| } |
| |
| return nullptr; |
| })); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| GURL google_url = |
| embedded_test_server()->GetURL("google.com", "/extensions/body1.html"); |
| |
| // First, check normal requests (e.g., navigations) to verify the extension |
| // is working correctly. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, google_url)); |
| EXPECT_EQ(google_url, web_contents->GetLastCommittedURL()); |
| |
| // google.com should succeed. |
| EXPECT_EQ(kGoogleBodyContent, |
| content::EvalJs(web_contents, "document.body.textContent.trim();")); |
| |
| GURL example_url = |
| embedded_test_server()->GetURL("example.com", "/extensions/body2.html"); |
| |
| // Navigation to example.com should fail. |
| ASSERT_FALSE(NavigateToURL(web_contents, example_url)); |
| { |
| content::NavigationEntry* nav_entry = |
| web_contents->GetController().GetLastCommittedEntry(); |
| ASSERT_TRUE(nav_entry); |
| EXPECT_EQ(content::PAGE_TYPE_ERROR, nav_entry->GetPageType()); |
| EXPECT_NE( |
| kExampleBodyContent, |
| content::EvalJs(web_contents, "document.body.textContent.trim();")); |
| } |
| |
| // A callback allow waiting for a response to complete with an expected status |
| // and given content. |
| auto make_browser_request = |
| [](network::mojom::URLLoaderFactory* url_loader_factory, const GURL& url, |
| const std::optional<std::string>& expected_response, |
| int expected_net_code) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = url; |
| request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| |
| content::SimpleURLLoaderTestHelper simple_loader_helper; |
| auto simple_loader = network::SimpleURLLoader::Create( |
| std::move(request), TRAFFIC_ANNOTATION_FOR_TESTS); |
| simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory, simple_loader_helper.GetCallbackDeprecated()); |
| |
| simple_loader_helper.WaitForCallback(); |
| |
| if (expected_response.has_value()) { |
| EXPECT_TRUE(!!simple_loader_helper.response_body()); |
| EXPECT_EQ(*simple_loader_helper.response_body(), *expected_response); |
| EXPECT_EQ(200, |
| simple_loader->ResponseInfo()->headers->response_code()); |
| } else { |
| EXPECT_FALSE(!!simple_loader_helper.response_body()); |
| EXPECT_EQ(simple_loader->NetError(), expected_net_code); |
| } |
| }; |
| |
| // Next, try a series of requests through URLRequestFetchers (rather than a |
| // renderer). |
| auto* url_loader_factory = profile() |
| ->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess() |
| .get(); |
| |
| { |
| // google.com should be unaffected by the extension and should succeed. |
| SCOPED_TRACE("google.com with Profile's url loader"); |
| make_browser_request(url_loader_factory, google_url, kGoogleFullContent, |
| net::OK); |
| } |
| |
| { |
| // example.com should also succeed since non-navigation browser-initiated |
| // requests are hidden from extensions. See crbug.com/884932. |
| SCOPED_TRACE("example.com with Profile's url loader"); |
| make_browser_request(url_loader_factory, example_url, kExampleFullContent, |
| net::OK); |
| } |
| |
| // Requests going through the system network context manager should always |
| // succeed. |
| SystemNetworkContextManager* system_network_context_manager = |
| g_browser_process->system_network_context_manager(); |
| network::mojom::URLLoaderFactory* system_url_loader_factory = |
| system_network_context_manager->GetURLLoaderFactory(); |
| { |
| // google.com should succeed (again). |
| SCOPED_TRACE("google.com with System's network context manager"); |
| make_browser_request(system_url_loader_factory, google_url, |
| kGoogleFullContent, net::OK); |
| } |
| { |
| // example.com should also succeed, since it's not through the profile's |
| // request context. |
| SCOPED_TRACE("example.com with System's network context manager"); |
| make_browser_request(system_url_loader_factory, example_url, |
| kExampleFullContent, net::OK); |
| } |
| } |
| |
| // Test that extensions need host permissions to both the request url and |
| // initiator to intercept a request. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextTypeMV3, |
| InitiatorAccessRequired) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("webrequest_permissions/initiator")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| |
| struct TestCase { |
| std::string navigate_before_start; |
| std::string xhr_domain; |
| std::string expected_initiator; |
| } testcases[] = {{"example.com", "example.com", "example.com"}, |
| {"example2.com", "example3.com", "example2.com"}, |
| // No access to the initiator. |
| {"no-permission.com", "example4.com", ""}, |
| // No access to the request url. |
| {"example.com", "no-permission.com", ""}}; |
| |
| int port = embedded_test_server()->port(); |
| |
| int expected_requests_intercepted_count = 0; |
| for (const auto& testcase : testcases) { |
| SCOPED_TRACE(testcase.navigate_before_start + ":" + testcase.xhr_domain + |
| ":" + testcase.expected_initiator); |
| ExtensionTestMessageListener initiator_listener; |
| initiator_listener.set_extension_id(extension->id()); |
| ASSERT_TRUE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| testcase.navigate_before_start, |
| "/extensions/body1.html"))); |
| content::WaitForLoadStop(web_contents); |
| PerformXhrInFrame(web_contents->GetPrimaryMainFrame(), testcase.xhr_domain, |
| port, "extensions/api_test/webrequest/xhr/data.json"); |
| |
| // Ensure that the extension wasn't able to intercept the request if it |
| // didn't have permission to the initiator or the request url. |
| if (!testcase.expected_initiator.empty()) { |
| ++expected_requests_intercepted_count; |
| } |
| |
| // Run a script in the extensions background page to ensure that we have |
| // received the initiator message from the extension. |
| ASSERT_EQ(expected_requests_intercepted_count, |
| GetCountFromBackgroundScript(extension, profile(), |
| "self.requestsIntercepted")); |
| |
| if (testcase.expected_initiator.empty()) { |
| EXPECT_FALSE(initiator_listener.was_satisfied()); |
| } else { |
| ASSERT_TRUE(initiator_listener.was_satisfied()); |
| EXPECT_EQ("http://" + testcase.expected_initiator + ":" + |
| base::NumberToString(port), |
| initiator_listener.message()); |
| } |
| } |
| } |
| |
| // TODO(crbug.com/409180663): test is failing on MSan, UBSan and ASan |
| #if defined(MEMORY_SANITIZER) || defined(UNDEFINED_SANITIZER) || \ |
| defined(ADDRESS_SANITIZER) |
| #define MAYBE_WebRequestApiDoesNotCrashOnErrorAfterProfileDestroyed \ |
| DISABLED_WebRequestApiDoesNotCrashOnErrorAfterProfileDestroyed |
| #else |
| #define MAYBE_WebRequestApiDoesNotCrashOnErrorAfterProfileDestroyed \ |
| WebRequestApiDoesNotCrashOnErrorAfterProfileDestroyed |
| #endif // defined(MEMORY_SANITIZER) || defined(UNDEFINED_SANITIZER) || |
| // defined(ADDRESS_SANITIZER) |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Regression test for http://crbug.com/878366. |
| // TODO(crbug.com/371324825): Port to desktop Android. The test crashes during |
| // Profile creation because Android requires a "startup data profile key". |
| IN_PROC_BROWSER_TEST_F( |
| ExtensionWebRequestApiTest, |
| MAYBE_WebRequestApiDoesNotCrashOnErrorAfterProfileDestroyed) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a profile that will be destroyed later. |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| #if BUILDFLAG(IS_CHROMEOS) |
| ash::ProfileHelper::SetAlwaysReturnPrimaryUserForTesting(true); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| ProfileManager* profile_manager = g_browser_process->profile_manager(); |
| std::unique_ptr<Profile> temp_profile = Profile::CreateProfile( |
| profile_manager->user_data_dir().AppendASCII("profile"), nullptr, |
| Profile::CreateMode::kSynchronous); |
| // Create a WebRequestAPI instance that we can control the lifetime of. |
| auto api = std::make_unique<WebRequestAPI>(temp_profile.get()); |
| // Make sure we are proxying for |temp_profile|. |
| api->ForceProxyForTesting(); |
| temp_profile->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting(); |
| |
| network::URLLoaderFactoryBuilder factory_builder; |
| |
| auto temp_web_contents = |
| WebContents::Create(WebContents::CreateParams(temp_profile.get())); |
| content::RenderFrameHost* frame = temp_web_contents->GetPrimaryMainFrame(); |
| EXPECT_TRUE(api->MaybeProxyURLLoaderFactory( |
| frame->GetProcess()->GetBrowserContext(), frame, |
| frame->GetProcess()->GetDeprecatedID(), |
| content::ContentBrowserClient::URLLoaderFactoryType::kDocumentSubResource, |
| std::nullopt, ukm::kInvalidSourceIdObj, factory_builder, nullptr, |
| nullptr)); |
| temp_web_contents.reset(); |
| auto params = network::mojom::URLLoaderFactoryParams::New(); |
| params->process_id = 0; |
| mojo::Remote<network::mojom::URLLoaderFactory> factory( |
| std::move(factory_builder) |
| .Finish<mojo::PendingRemote<network::mojom::URLLoaderFactory>>( |
| temp_profile->GetDefaultStoragePartition()->GetNetworkContext(), |
| std::move(params))); |
| |
| network::TestURLLoaderClient client; |
| mojo::PendingRemote<network::mojom::URLLoader> loader; |
| network::ResourceRequest resource_request; |
| resource_request.url = embedded_test_server()->GetURL("/hung"); |
| factory->CreateLoaderAndStart( |
| loader.InitWithNewPipeAndPassReceiver(), 0, |
| network::mojom::kURLLoadOptionNone, resource_request, |
| client.CreateRemote(), |
| net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); |
| |
| // Destroy profile, unbind client to cause a connection error, and delete the |
| // WebRequestAPI. This will cause the connection error that will reach the |
| // proxy before the ProxySet shutdown code runs on the IO thread. |
| api->Shutdown(); |
| |
| // We are about to destroy a profile. In production that will only happen |
| // as part of the destruction of BrowserProcess's ProfileManager. This |
| // happens in PostMainMessageLoopRun(). This means that to have this test |
| // represent production we have to make sure that no tasks are pending on the |
| // main thread before we destroy the profile. We also would need to prohibit |
| // the posting of new tasks on the main thread as in production the main |
| // thread's message loop will not be accepting them. We fallback on flushing |
| // the ThreadPool here to avoid the posts coming from it. |
| content::RunAllTasksUntilIdle(); |
| |
| ProfileDestroyer::DestroyOriginalProfileWhenAppropriate( |
| std::move(temp_profile)); |
| client.Unbind(); |
| api.reset(); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests that webRequest API can inspect window.open() requests initiated from |
| // chrome-untrusted:// pages to Web origins, but not other WebUI origins. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| OpenNewTabFromChromeUntrusted) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test")); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test2")); |
| |
| // Loads a test extension. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Opens a chrome-untrusted:// page. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, GURL("chrome-untrusted://test/title1.html"))); |
| content::WaitForLoadStop(web_contents); |
| auto* rfh = web_contents->GetPrimaryMainFrame(); |
| |
| { |
| // Trigger a `window.open()` to a Web origin from chrome-untrusted:// page. |
| const GURL web_url = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| content::TestNavigationObserver navigation_observer(web_url); |
| navigation_observer.StartWatchingNewWebContents(); |
| ASSERT_TRUE(content::ExecJs( |
| rfh, content::JsReplace("window.open($1, '_blank');", web_url.spec()))); |
| navigation_observer.Wait(); |
| ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| |
| // The extension should see the request to the Web origin. |
| EXPECT_TRUE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| web_url.GetHost())); |
| } |
| |
| { |
| // Trigger a `window.open()` to a WebUI origin from chrome-untrusted:// |
| // page. |
| const GURL webui_url = GURL("chrome-untrusted://test2/title2.html"); |
| content::TestNavigationObserver navigation_observer(webui_url); |
| navigation_observer.StartWatchingNewWebContents(); |
| ASSERT_TRUE(content::ExecJs( |
| rfh, |
| content::JsReplace("window.open($1, '_blank');", webui_url.spec()))); |
| navigation_observer.Wait(); |
| ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| |
| // The extension shouldn't see the request to the WebUI pages. |
| EXPECT_FALSE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| webui_url.GetHost())); |
| } |
| } |
| |
| // Tests that webRequest API can inspect a chrome-untrusted:// main frame |
| // navigating itself to Web origins. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| NavigateMainFrameToWebOriginFromChromeUntrusted) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test")); |
| |
| // Loads a test extension. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Opens a chrome-untrusted:// page. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, GURL("chrome-untrusted://test/title1.html"))); |
| content::WaitForLoadStop(web_contents); |
| auto* rfh = web_contents->GetPrimaryMainFrame(); |
| |
| // Navigate the main frame itself to Web origin, this extension should see |
| // the request. |
| const auto web_url = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| content::TestNavigationObserver navigation_observer(web_url); |
| navigation_observer.WatchExistingWebContents(); |
| ASSERT_TRUE(content::ExecJs( |
| rfh, content::JsReplace("location.href=$1;", web_url.spec()))); |
| navigation_observer.Wait(); |
| ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_TRUE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| web_url.GetHost())); |
| } |
| |
| // Tests that webRequest API can't inspect a chrome-untrusted:// main frame |
| // navigating itself to another WebUI origin. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| NavigateMainFrameToWebUIOriginFromChromeUntrusted) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test")); |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test2")); |
| |
| // Loads a test extension. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Opens a chrome-untrusted:// page. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, GURL("chrome-untrusted://test/title1.html"))); |
| content::WaitForLoadStop(web_contents); |
| auto* rfh = web_contents->GetPrimaryMainFrame(); |
| |
| // Navigate the main frame itself to Web origin, this extension should see |
| // the request. |
| const auto webui_url = GURL("chrome-untrusted://test2/title2.html"); |
| content::TestNavigationObserver navigation_observer(webui_url); |
| navigation_observer.WatchExistingWebContents(); |
| ASSERT_TRUE(content::ExecJs( |
| rfh, content::JsReplace("location.href=$1;", webui_url.spec()))); |
| navigation_observer.Wait(); |
| ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_FALSE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| webui_url.GetHost())); |
| } |
| |
| // Tests that webRequest API can't inspect a subframe inside chrome-untrusted:// |
| // navigating to a Web origin. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| SubframeNavigationsInChromeUntrustedPage) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| // Allow embedding child frames; |
| content::TestUntrustedDataSourceHeaders headers; |
| headers.child_src = "child-src *;"; |
| content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( |
| std::make_unique<ui::TestUntrustedWebUIConfig>("test", headers)); |
| |
| // Loads a test extension. |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Opens a chrome-untrusted:// page. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, GURL("chrome-untrusted://test/title1.html"))); |
| content::WaitForLoadStop(web_contents); |
| auto* rfh = web_contents->GetPrimaryMainFrame(); |
| |
| // Start a subframe navigation to Web origin. |
| const auto web_url = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| content::TestNavigationObserver navigation_observer(web_url); |
| navigation_observer.WatchExistingWebContents(); |
| ASSERT_TRUE(content::ExecJs(rfh, content::JsReplace(R"javascript( |
| const el = document.createElement("iframe"); |
| document.body.appendChild(el); |
| el.src = $1; |
| )javascript", |
| web_url.spec()))); |
| navigation_observer.Wait(); |
| |
| ASSERT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_FALSE(HasSeenWebRequestInBackgroundScript(extension, profile(), |
| web_url.GetHost())); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Test fixture which sets a custom NTP Page. |
| // Not ported to desktop Android because the Android NTP is native UI. |
| class NTPInterceptionWebRequestAPITest |
| : public ExtensionApiTest, |
| public testing::WithParamInterface<ContextType> { |
| public: |
| NTPInterceptionWebRequestAPITest() |
| : ExtensionApiTest(GetParam()), |
| https_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} |
| |
| NTPInterceptionWebRequestAPITest(const NTPInterceptionWebRequestAPITest&) = |
| delete; |
| NTPInterceptionWebRequestAPITest& operator=( |
| const NTPInterceptionWebRequestAPITest&) = delete; |
| ~NTPInterceptionWebRequestAPITest() override = default; |
| |
| // ExtensionApiTest override: |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| test_data_dir_ = test_data_dir_.AppendASCII("webrequest") |
| .AppendASCII("ntp_request_interception"); |
| https_test_server_.ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(https_test_server_.Start()); |
| |
| GURL ntp_url = https_test_server_.GetURL("/fake_ntp.html"); |
| ntp_test_utils::SetUserSelectedDefaultSearchProvider( |
| profile(), https_test_server_.base_url().spec(), ntp_url.spec()); |
| } |
| |
| const net::EmbeddedTestServer* https_test_server() const { |
| return &https_test_server_; |
| } |
| |
| private: |
| net::EmbeddedTestServer https_test_server_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(PersistentBackground, |
| NTPInterceptionWebRequestAPITest, |
| ::testing::Values(ContextType::kPersistentBackground)); |
| |
| INSTANTIATE_TEST_SUITE_P(ServiceWorker, |
| NTPInterceptionWebRequestAPITest, |
| ::testing::Values(ContextType::kServiceWorker)); |
| |
| // Ensures that requests made by the NTP Instant renderer are hidden from the |
| // Web Request API. Regression test for crbug.com/797461. |
| IN_PROC_BROWSER_TEST_P(NTPInterceptionWebRequestAPITest, |
| NTPRendererRequestsHidden) { |
| // Loads an extension which tries to intercept requests to |
| // "fake_ntp_script.js", which will be loaded as part of the NTP renderer. |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("extension")); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| // Wait for webRequest listeners to be set up. |
| profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting(); |
| |
| // Have the extension listen for requests to |fake_ntp_script.js|. |
| listener.Reply(https_test_server()->GetURL("/fake_ntp_script.js").spec()); |
| |
| // Returns true if the given extension was able to intercept the request to |
| // "fake_ntp_script.js". |
| auto was_script_request_intercepted = |
| [this](const ExtensionId& extension_id) { |
| const std::optional<bool> result = ExecuteScriptAndReturnBool( |
| extension_id, profile(), "getAndResetRequestIntercepted();"); |
| DCHECK(result); |
| return *result; |
| }; |
| |
| // Returns true if the given |web_contents| has window.scriptExecuted set to |
| // true; |
| auto was_ntp_script_loaded = [](content::WebContents* web_contents) { |
| return content::EvalJs(web_contents, "!!window.scriptExecuted;") |
| .ExtractBool(); |
| }; |
| |
| WebContents* web_contents = GetActiveWebContents(); |
| |
| // Navigate to the NTP. The request for "fake_ntp_script.js" should not have |
| // reached the extension, since it was made by the instant NTP renderer, which |
| // is semi-privileged. |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL(chrome::kChromeUINewTabURL))); |
| EXPECT_TRUE(was_ntp_script_loaded(web_contents)); |
| ASSERT_TRUE(search::IsInstantNTP(web_contents)); |
| EXPECT_FALSE(was_script_request_intercepted(extension->id())); |
| |
| // However, when a normal webpage requests the same script, the request should |
| // be seen by the extension. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, https_test_server()->GetURL("/page_with_ntp_script.html"))); |
| EXPECT_TRUE(was_ntp_script_loaded(web_contents)); |
| ASSERT_FALSE(search::IsInstantNTP(web_contents)); |
| EXPECT_TRUE(was_script_request_intercepted(extension->id())); |
| } |
| |
| // Test fixture testing that requests made for the OneGoogleBar on behalf of |
| // the WebUI NTP can't be intercepted by extensions. |
| class WebUiNtpInterceptionWebRequestAPITest |
| : public ExtensionApiTest, |
| public OneGoogleBarServiceObserver, |
| public testing::WithParamInterface<ContextType> { |
| public: |
| WebUiNtpInterceptionWebRequestAPITest() |
| : ExtensionApiTest(GetParam()), |
| https_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} |
| WebUiNtpInterceptionWebRequestAPITest( |
| const WebUiNtpInterceptionWebRequestAPITest&) = delete; |
| WebUiNtpInterceptionWebRequestAPITest& operator=( |
| const WebUiNtpInterceptionWebRequestAPITest&) = delete; |
| ~WebUiNtpInterceptionWebRequestAPITest() override = default; |
| |
| // ExtensionApiTest override: |
| void SetUp() override { |
| https_test_server_.RegisterRequestMonitor(base::BindRepeating( |
| &WebUiNtpInterceptionWebRequestAPITest::MonitorRequest, |
| base::Unretained(this))); |
| ASSERT_TRUE(https_test_server_.InitializeAndListen()); |
| ExtensionApiTest::SetUp(); |
| } |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII(switches::kGoogleBaseURL, |
| https_test_server_.base_url().spec()); |
| } |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| |
| https_test_server_.StartAcceptingConnections(); |
| |
| one_google_bar_url_ = |
| one_google_bar_service()->loader_for_testing()->GetLoadURLForTesting(); |
| |
| // Can't declare |runloop_| as a data member on the stack since it needs to |
| // be be constructed from a single-threaded context. |
| runloop_ = std::make_unique<base::RunLoop>(); |
| one_google_bar_service()->AddObserver(this); |
| } |
| |
| // OneGoogleBarServiceObserver overrides: |
| void OnOneGoogleBarDataUpdated() override { runloop_->Quit(); } |
| void OnOneGoogleBarServiceShuttingDown() override { |
| one_google_bar_service()->RemoveObserver(this); |
| } |
| |
| GURL one_google_bar_url() const { return one_google_bar_url_; } |
| |
| // Waits for OneGoogleBar data to be updated. Should only be used once. |
| void WaitForOneGoogleBarDataUpdate() { runloop_->Run(); } |
| |
| bool GetAndResetOneGoogleBarRequestSeen() { |
| base::AutoLock lock(lock_); |
| bool result = one_google_bar_request_seen_; |
| one_google_bar_request_seen_ = false; |
| return result; |
| } |
| |
| private: |
| OneGoogleBarService* one_google_bar_service() { |
| return OneGoogleBarServiceFactory::GetForProfile(profile()); |
| } |
| |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| if (request.GetURL() == one_google_bar_url_) { |
| base::AutoLock lock(lock_); |
| one_google_bar_request_seen_ = true; |
| } |
| } |
| |
| net::EmbeddedTestServer https_test_server_; |
| std::unique_ptr<base::RunLoop> runloop_; |
| |
| // Initialized on the UI thread in SetUpOnMainThread. Read on UI and Embedded |
| // Test Server IO thread thereafter. |
| GURL one_google_bar_url_; |
| |
| // Accessed on multiple threads- UI and Embedded Test Server IO thread. Access |
| // requires acquiring |lock_|. |
| bool one_google_bar_request_seen_ = false; |
| |
| base::Lock lock_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(PersistentBackground, |
| WebUiNtpInterceptionWebRequestAPITest, |
| ::testing::Values(ContextType::kPersistentBackground)); |
| |
| INSTANTIATE_TEST_SUITE_P(ServiceWorker, |
| WebUiNtpInterceptionWebRequestAPITest, |
| ::testing::Values(ContextType::kServiceWorker)); |
| |
| // TODO(https://crbug.com/407399340): Failing on various bots; |
| // msan, asan, dbg, code coverage, chromeos-rel. |
| IN_PROC_BROWSER_TEST_P(WebUiNtpInterceptionWebRequestAPITest, |
| DISABLED_OneGoogleBarRequestsHidden) { |
| // Loads an extension which tries to intercept requests to the OneGoogleBar. |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest") |
| .AppendASCII("ntp_request_interception") |
| .AppendASCII("extension")); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Have the extension listen for requests to |one_google_bar_url()|. |
| listener.Reply(one_google_bar_url().spec()); |
| |
| // Returns true if the given extension was able to intercept the request to |
| // |one_google_bar_url()|. |
| auto was_script_request_intercepted = |
| [this](const ExtensionId& extension_id) { |
| std::optional<bool> result = ExecuteScriptAndReturnBool( |
| extension_id, profile(), "getAndResetRequestIntercepted();"); |
| DCHECK(result); |
| return *result; |
| }; |
| |
| ASSERT_FALSE(GetAndResetOneGoogleBarRequestSeen()); |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL(chrome::kChromeUINewTabURL))); |
| ASSERT_EQ(ntp_test_utils::GetFinalNtpUrl(profile()), |
| GetActiveWebContents()->GetLastCommittedURL()); |
| WaitForOneGoogleBarDataUpdate(); |
| ASSERT_TRUE(GetAndResetOneGoogleBarRequestSeen()); |
| |
| // Ensure that the extension wasn't able to intercept the request. |
| EXPECT_FALSE(was_script_request_intercepted(extension->id())); |
| |
| // A normal request to |one_google_bar_url()| (i.e. not made by |
| // OneGoogleBarFetcher) should be intercepted by extensions. |
| ASSERT_TRUE(NavigateToURL(web_contents, one_google_bar_url())); |
| EXPECT_TRUE(was_script_request_intercepted(extension->id())); |
| ASSERT_TRUE(GetAndResetOneGoogleBarRequestSeen()); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| class WebRequestApiTestWithManagementPolicy |
| : public ExtensionApiTestWithManagementPolicy, |
| public testing::WithParamInterface<ContextType> { |
| public: |
| WebRequestApiTestWithManagementPolicy() |
| : ExtensionApiTestWithManagementPolicy(GetParam()) {} |
| ~WebRequestApiTestWithManagementPolicy() override = default; |
| WebRequestApiTestWithManagementPolicy( |
| const WebRequestApiTestWithManagementPolicy&) = delete; |
| WebRequestApiTestWithManagementPolicy& operator=( |
| const WebRequestApiTestWithManagementPolicy&) = delete; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(PersistentBackground, |
| WebRequestApiTestWithManagementPolicy, |
| ::testing::Values(ContextType::kPersistentBackground)); |
| INSTANTIATE_TEST_SUITE_P(ServiceWorker, |
| WebRequestApiTestWithManagementPolicy, |
| ::testing::Values(ContextType::kServiceWorker)); |
| |
| // Tests that the webRequest events aren't dispatched when the request initiator |
| // is protected by policy. |
| IN_PROC_BROWSER_TEST_P(WebRequestApiTestWithManagementPolicy, |
| InitiatorProtectedByPolicy) { |
| // We expect that no request will be hidden or modification blocked. This |
| // means that the request to example.com will be seen by the extension. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://notexample.com"); |
| } |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Host navigated to. |
| const std::string example_com = "example.com"; |
| |
| // URL of a page that initiates a cross domain requests when navigated to. |
| const GURL extension_test_url = embedded_test_server()->GetURL( |
| example_com, |
| "/extensions/api_test/webrequest/policy_blocked/ref_remote_js.html"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest/policy_blocked")); |
| ASSERT_TRUE(extension) << message_; |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Extension communicates back using this listener name. |
| const std::string listener_message = "protected_origin"; |
| |
| // The number of requests initiated by a protected origin is tracked in |
| // the extension's background page under this variable name. |
| const std::string request_counter_name = "self.protectedOriginCount"; |
| |
| EXPECT_EQ(0, GetCountFromBackgroundScript(extension, profile(), |
| request_counter_name)); |
| |
| // Wait until all remote Javascript files have been blocked / pulled down. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, extension_test_url)); |
| content::WaitForLoadStop(web_contents); |
| |
| // Domain that hosts javascript file referenced by example_com. |
| const std::string example2_com = "example2.com"; |
| |
| // The server saw a request for the remote Javascript file. |
| EXPECT_TRUE(BrowsedTo(example2_com)); |
| |
| // The request was seen by the extension. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| request_counter_name)); |
| |
| // Clear the list of domains the server has seen. |
| ClearRequestLog(); |
| |
| // Make sure we've cleared the embedded server history. |
| EXPECT_FALSE(BrowsedTo(example2_com)); |
| |
| // Set the policy to hide requests to example.com or any resource |
| // it includes. We expect that in this test, the request to example2.com |
| // will not be seen by the extension. |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://" + example_com); |
| } |
| |
| // Wait until all remote Javascript files have been pulled down. |
| ASSERT_TRUE(NavigateToURL(web_contents, extension_test_url)); |
| content::WaitForLoadStop(web_contents); |
| |
| // The server saw a request for the remote Javascript file. |
| EXPECT_TRUE(BrowsedTo(example2_com)); |
| |
| // The request was hidden from the extension. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| request_counter_name)); |
| } |
| |
| // Tests that the webRequest events aren't dispatched when the URL of the |
| // request is protected by policy. |
| IN_PROC_BROWSER_TEST_P(WebRequestApiTestWithManagementPolicy, |
| UrlProtectedByPolicy) { |
| // Host protected by policy. |
| const std::string protected_domain = "example.com"; |
| |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://" + protected_domain); |
| } |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| LoadExtension(test_data_dir_.AppendASCII("webrequest/policy_blocked")); |
| |
| // Listen in case extension sees the request. |
| ExtensionTestMessageListener before_request_listener("protected_url"); |
| |
| // Path to resolve during test navigations. |
| const std::string test_path = "/defaultresponse?protected_url"; |
| |
| // Navigate to the protected domain and wait until page fully loads. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| protected_domain, test_path))); |
| content::WaitForLoadStop(web_contents); |
| |
| // The server saw a request for the protected site. |
| EXPECT_TRUE(BrowsedTo(protected_domain)); |
| |
| // The request was hidden from the extension. |
| EXPECT_FALSE(before_request_listener.was_satisfied()); |
| |
| // Host not protected by policy. |
| const std::string unprotected_domain = "notblockedexample.com"; |
| |
| // Now we'll test browsing to a non-protected website where we expect the |
| // extension to see the request. |
| ASSERT_TRUE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| unprotected_domain, test_path))); |
| content::WaitForLoadStop(web_contents); |
| |
| // The server saw a request for the non-protected site. |
| EXPECT_TRUE(BrowsedTo(unprotected_domain)); |
| |
| // The request was visible from the extension. |
| EXPECT_TRUE(before_request_listener.was_satisfied()); |
| } |
| |
| // Test that no webRequest events are seen for a protected host during normal |
| // navigation. This replicates most of the tests from |
| // WebRequestWithWithheldPermissions with a protected host. Granting a tab |
| // specific permission shouldn't bypass our policy. |
| IN_PROC_BROWSER_TEST_P(WebRequestApiTestWithManagementPolicy, |
| WebRequestProtectedByPolicy) { |
| // Host protected by policy. |
| const std::string protected_domain = "example.com"; |
| |
| { |
| ExtensionManagementPolicyUpdater pref(&policy_provider_); |
| pref.AddPolicyBlockedHost("*", "*://" + protected_domain); |
| } |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_activetab")); |
| ASSERT_TRUE(extension) << message_; |
| ScriptingPermissionsModifier(profile(), base::WrapRefCounted(extension)) |
| .SetWithholdHostPermissions(true); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // Navigate the browser to a page in a new tab. |
| NavigateToURLInNewTab( |
| embedded_test_server()->GetURL(protected_domain, "/empty.html")); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| ExtensionActionRunner* runner = |
| ExtensionActionRunner::GetForWebContents(web_contents); |
| ASSERT_TRUE(runner); |
| int port = embedded_test_server()->port(); |
| const std::string kXhrPath = "simple.html"; |
| |
| // The extension shouldn't have currently received any webRequest events, |
| // since it doesn't have permission (and shouldn't receive any from an XHR). |
| EXPECT_EQ(0, GetWebRequestCountFromBackgroundScript(extension, profile())); |
| PerformXhrInFrame(web_contents->GetPrimaryMainFrame(), protected_domain, port, |
| kXhrPath); |
| EXPECT_EQ(0, GetWebRequestCountFromBackgroundScript(extension, profile())); |
| |
| // Grant activeTab permission, and perform another XHR. The extension should |
| // still be blocked due to ExtensionSettings policy on example.com. |
| // Only records ACCESS_WITHHELD, not ACCESS_DENIED, this is why it matches |
| // BLOCKED_ACTION_NONE. |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| auto reload_page_dialog_reset = |
| ReloadPageDialogController::AcceptDialogForTesting(true); |
| runner->RunAction(extension, true); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| EXPECT_EQ(BLOCKED_ACTION_NONE, runner->GetBlockedActions(extension->id())); |
| int xhr_count = GetWebRequestCountFromBackgroundScript(extension, profile()); |
| // ... which means that we should have a non-zero xhr count if the policy |
| // didn't block the events. |
| EXPECT_EQ(0, xhr_count); |
| // And the extension should also block future events. |
| PerformXhrInFrame(web_contents->GetPrimaryMainFrame(), protected_domain, port, |
| kXhrPath); |
| EXPECT_EQ(0, GetWebRequestCountFromBackgroundScript(extension, profile())); |
| } |
| |
| // A test fixture which mocks the Time::Now() function to ensure that the |
| // default clock returns monotonically increasing time. |
| class ExtensionWebRequestMockedClockTest |
| : public ExtensionWebRequestApiTestWithContextType { |
| public: |
| ExtensionWebRequestMockedClockTest() |
| : scoped_time_clock_override_(&ExtensionWebRequestMockedClockTest::Now, |
| nullptr, |
| nullptr) {} |
| |
| ExtensionWebRequestMockedClockTest( |
| const ExtensionWebRequestMockedClockTest&) = delete; |
| ExtensionWebRequestMockedClockTest& operator=( |
| const ExtensionWebRequestMockedClockTest&) = delete; |
| |
| private: |
| static base::Time Now() { |
| static base::Time now_time = base::Time::UnixEpoch(); |
| now_time += base::Milliseconds(1); |
| return now_time; |
| } |
| |
| base::subtle::ScopedTimeClockOverrides scoped_time_clock_override_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ExtensionWebRequestMockedClockTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ExtensionWebRequestMockedClockTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorkerMV2, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| // Tests that we correctly dispatch the OnActionIgnored event on an extension |
| // if the extension's proposed redirect is ignored. |
| // TODO(crbug.com/391920604): Port to desktop Android when ReloadExtension() |
| // is supported. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestMockedClockTest, |
| OnActionIgnored_Redirect) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Load the two extensions. They redirect "google.com" main-frame urls to |
| // the corresponding "example.com and "foo.com" urls. |
| base::FilePath test_dir = |
| test_data_dir_.AppendASCII("webrequest/on_action_ignored"); |
| |
| // Load the first extension. |
| ExtensionTestMessageListener ready_1_listener("ready_1"); |
| const Extension* extension_1 = |
| LoadExtension(test_dir.AppendASCII("extension_1")); |
| ASSERT_TRUE(extension_1); |
| ASSERT_TRUE(ready_1_listener.WaitUntilSatisfied()); |
| const ExtensionId extension_id_1 = extension_1->id(); |
| |
| // Load the second extension. |
| ExtensionTestMessageListener ready_2_listener("ready_2"); |
| const Extension* extension_2 = |
| LoadExtension(test_dir.AppendASCII("extension_2")); |
| ASSERT_TRUE(extension_2); |
| ASSERT_TRUE(ready_2_listener.WaitUntilSatisfied()); |
| const ExtensionId extension_id_2 = extension_2->id(); |
| |
| const ExtensionPrefs* prefs = ExtensionPrefs::Get(profile()); |
| EXPECT_LT(GetLastUpdateTime(prefs, extension_id_1), |
| GetLastUpdateTime(prefs, extension_id_2)); |
| |
| // The extensions will notify the browser if their proposed redirect was |
| // successful or not. |
| ExtensionTestMessageListener redirect_ignored_listener("redirect_ignored"); |
| ExtensionTestMessageListener redirect_successful_listener( |
| "redirect_successful"); |
| |
| const GURL url = embedded_test_server()->GetURL("google.com", "/simple.html"); |
| const GURL expected_redirect_url_1 = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| const GURL expected_redirect_url_2 = |
| embedded_test_server()->GetURL("foo.com", "/simple.html"); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // The second extension is the latest installed, hence it's redirect url |
| // should take precedence. |
| EXPECT_EQ(expected_redirect_url_2, web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(redirect_ignored_listener.WaitUntilSatisfied()); |
| EXPECT_EQ(extension_id_1, |
| redirect_ignored_listener.extension_id_for_message()); |
| EXPECT_TRUE(redirect_successful_listener.WaitUntilSatisfied()); |
| EXPECT_EQ(extension_id_2, |
| redirect_successful_listener.extension_id_for_message()); |
| |
| // Now let |extension_1| be installed after |extension_2|. For an unpacked |
| // extension, reloading is equivalent to a reinstall. |
| ready_1_listener.Reset(); |
| ReloadExtension(extension_id_1); |
| ASSERT_TRUE(ready_1_listener.WaitUntilSatisfied()); |
| |
| EXPECT_LT(GetLastUpdateTime(prefs, extension_id_2), |
| GetLastUpdateTime(prefs, extension_id_1)); |
| |
| redirect_ignored_listener.Reset(); |
| redirect_successful_listener.Reset(); |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // The first extension is the latest installed, hence it's redirect url |
| // should take precedence. |
| EXPECT_EQ(expected_redirect_url_1, web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(redirect_ignored_listener.WaitUntilSatisfied()); |
| EXPECT_EQ(extension_id_2, |
| redirect_ignored_listener.extension_id_for_message()); |
| EXPECT_TRUE(redirect_successful_listener.WaitUntilSatisfied()); |
| EXPECT_EQ(extension_id_1, |
| redirect_successful_listener.extension_id_for_message()); |
| } |
| |
| // Regression test for http://crbug.com/1074282. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| StaleHeadersAfterRedirect) { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Stale Headers Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| self.locationCount = 0; |
| self.requestCount = 0; |
| chrome.test.sendMessage('ready', function(reply) { |
| chrome.webRequest.onResponseStarted.addListener(function(details) { |
| self.requestCount++; |
| var headers = details.responseHeaders; |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].name === 'Location') { |
| self.locationCount++; |
| } |
| } |
| }, |
| {urls: ['<all_urls>'], types: ['xmlhttprequest']}, |
| ['responseHeaders', 'extraHeaders'] |
| ); |
| }); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| auto task_runner = base::SequencedTaskRunner::GetCurrentDefault(); |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url != "/redirect-and-wait") { |
| return nullptr; |
| } |
| |
| // Wait for the listener to be installed before proceeding. |
| base::WaitableEvent unblock( |
| base::WaitableEvent::ResetPolicy::AUTOMATIC, |
| base::WaitableEvent::InitialState::NOT_SIGNALED); |
| // Post a task to the UI thread since the request handler runs on a |
| // background thread. |
| task_runner->PostTask(FROM_HERE, base::BindLambdaForTesting([&] { |
| listener.Reply(""); |
| WaitForExtraHeadersListener(&unblock, |
| profile()); |
| })); |
| unblock.Wait(); |
| |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_MOVED_PERMANENTLY); |
| http_response->AddCustomHeader( |
| "Location", embedded_test_server()->GetURL("/echo").spec()); |
| return http_response; |
| })); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Navigate to a basic page so XHR requests work. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, embedded_test_server()->GetURL("/echo"))); |
| content::WaitForLoadStop(web_contents); |
| |
| // Make a XHR request which redirects. The final response should not include |
| // the Location header. |
| PerformXhrInFrame(web_contents->GetPrimaryMainFrame(), |
| embedded_test_server()->host_port_pair().host(), |
| embedded_test_server()->port(), "redirect-and-wait"); |
| EXPECT_EQ( |
| GetCountFromBackgroundScript(extension, profile(), "self.requestCount"), |
| 1); |
| EXPECT_EQ( |
| GetCountFromBackgroundScript(extension, profile(), "self.locationCount"), |
| 0); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| ChangeHeaderUMAs) { |
| using RequestHeaderType = |
| extension_web_request_api_helpers::RequestHeaderType; |
| using ResponseHeaderType = |
| extension_web_request_api_helpers::ResponseHeaderType; |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request UMA Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onBeforeSendHeaders.addListener(function(details) { |
| var headers = details.requestHeaders; |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'user-agent') { |
| headers[i].value = 'foo'; |
| break; |
| } |
| } |
| headers.push({name: 'Foo1', value: 'Bar1'}); |
| headers.push({name: 'Foo2', value: 'Bar2'}); |
| headers.push({name: 'DNT', value: '0'}); |
| return {requestHeaders: headers}; |
| }, {urls: ['*://*/set-cookie*']}, |
| ['blocking', 'requestHeaders', 'extraHeaders']); |
| |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| var headers = details.responseHeaders; |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'set-cookie' && |
| headers[i].value == 'key1=val1') { |
| headers.splice(i, 1); |
| i--; |
| } else if (headers[i].name == 'Content-Length') { |
| headers[i].value = '0'; |
| } |
| } |
| headers.push({name: 'Foo3', value: 'Bar3'}); |
| headers.push({name: 'Foo4', value: 'Bar4'}); |
| return {responseHeaders: headers}; |
| }, {urls: ['*://*/set-cookie*']}, |
| ['blocking', 'responseHeaders', 'extraHeaders']); |
| |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| base::HistogramTester tester; |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("/set-cookie?key1=val1&key2=val2"))); |
| content::WaitForLoadStop(web_contents); |
| |
| // Changed histograms should record kUserAgent request header along with |
| // kSetCookie and kContentLength response headers. |
| tester.ExpectUniqueSample("Extensions.WebRequest.RequestHeaderChanged", |
| RequestHeaderType::kUserAgent, 1); |
| EXPECT_THAT( |
| tester.GetAllSamples("Extensions.WebRequest.ResponseHeaderChanged"), |
| ::testing::UnorderedElementsAre( |
| base::Bucket(static_cast<base::HistogramBase::Sample32>( |
| ResponseHeaderType::kSetCookie), |
| 1), |
| base::Bucket(static_cast<base::HistogramBase::Sample32>( |
| ResponseHeaderType::kContentLength), |
| 1))); |
| |
| // Added request header histogram should record kOther and kDNT. |
| EXPECT_THAT(tester.GetAllSamples("Extensions.WebRequest.RequestHeaderAdded"), |
| ::testing::UnorderedElementsAre( |
| base::Bucket(static_cast<base::HistogramBase::Sample32>( |
| RequestHeaderType::kDnt), |
| 1), |
| base::Bucket(static_cast<base::HistogramBase::Sample32>( |
| RequestHeaderType::kOther), |
| 2))); |
| |
| // Added response header histogram should record kOther. |
| tester.ExpectUniqueSample("Extensions.WebRequest.ResponseHeaderAdded", |
| ResponseHeaderType::kOther, 2); |
| |
| // Histograms for removed headers should record kNone. |
| tester.ExpectUniqueSample("Extensions.WebRequest.RequestHeaderRemoved", |
| RequestHeaderType::kNone, 1); |
| tester.ExpectUniqueSample("Extensions.WebRequest.ResponseHeaderRemoved", |
| ResponseHeaderType::kNone, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| RemoveHeaderUMAs) { |
| using RequestHeaderType = |
| extension_web_request_api_helpers::RequestHeaderType; |
| using ResponseHeaderType = |
| extension_web_request_api_helpers::ResponseHeaderType; |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request UMA Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onBeforeSendHeaders.addListener(function(details) { |
| var headers = details.requestHeaders; |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'user-agent') { |
| headers.splice(i, 1); |
| break; |
| } |
| } |
| return {requestHeaders: headers}; |
| }, {urls: ['*://*/set-cookie*']}, |
| ['blocking', 'requestHeaders', 'extraHeaders']); |
| |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| var headers = details.responseHeaders; |
| for (var i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'set-cookie') { |
| headers.splice(i, 1); |
| break; |
| } |
| } |
| return {responseHeaders: headers}; |
| }, {urls: ['*://*/set-cookie*']}, |
| ['blocking', 'responseHeaders', 'extraHeaders']); |
| |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| base::HistogramTester tester; |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL("/set-cookie?Foo=Bar"))); |
| content::WaitForLoadStop(web_contents); |
| |
| // Histograms for removed headers should record kUserAgent and kSetCookie. |
| tester.ExpectUniqueSample("Extensions.WebRequest.RequestHeaderRemoved", |
| RequestHeaderType::kUserAgent, 1); |
| tester.ExpectUniqueSample("Extensions.WebRequest.ResponseHeaderRemoved", |
| ResponseHeaderType::kSetCookie, 1); |
| |
| // Histograms for changed headers should record kNone. |
| tester.ExpectUniqueSample("Extensions.WebRequest.RequestHeaderChanged", |
| RequestHeaderType::kNone, 1); |
| tester.ExpectUniqueSample("Extensions.WebRequest.ResponseHeaderChanged", |
| ResponseHeaderType::kNone, 1); |
| |
| // Histograms for added headers should record kNone. |
| tester.ExpectUniqueSample("Extensions.WebRequest.RequestHeaderAdded", |
| RequestHeaderType::kNone, 1); |
| tester.ExpectUniqueSample("Extensions.WebRequest.ResponseHeaderAdded", |
| ResponseHeaderType::kNone, 1); |
| } |
| |
| struct SWTestParams { |
| // This parameter is for opt_extraInfoSpec passed to addEventListener. |
| // 'blocking' and 'requestHeaders' if it's false, and 'extraHeaders' in |
| // addition to them if it's true. |
| bool extra_info_spec; |
| ContextType context_type; |
| }; |
| |
| class ServiceWorkerWebRequestApiTest |
| : public testing::WithParamInterface<SWTestParams>, |
| public ExtensionApiTest { |
| public: |
| ServiceWorkerWebRequestApiTest() |
| : ExtensionApiTest(GetParam().context_type) {} |
| ~ServiceWorkerWebRequestApiTest() override = default; |
| ServiceWorkerWebRequestApiTest(const ServiceWorkerWebRequestApiTest&) = |
| delete; |
| ServiceWorkerWebRequestApiTest& operator=( |
| const ServiceWorkerWebRequestApiTest&) = delete; |
| |
| // The options passed as opt_extraInfoSpec to addEventListener. |
| enum class ExtraInfoSpec { |
| // 'blocking', 'requestHeaders' |
| kDefault, |
| // kDefault + 'extraHeaders' |
| kExtraHeaders |
| }; |
| |
| static ExtraInfoSpec GetExtraInfoSpec() { |
| return GetParam().extra_info_spec ? ExtraInfoSpec::kExtraHeaders |
| : ExtraInfoSpec::kDefault; |
| } |
| |
| void InstallRequestHeaderModifyingExtension() { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Header Modifying Extension", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| |
| const char kBackgroundScript[] = R"( |
| chrome.webRequest.onBeforeSendHeaders.addListener(function(details) { |
| details.requestHeaders.push({name: 'foo', value: 'bar'}); |
| details.requestHeaders.push({ |
| name: 'frameId', |
| value: details.frameId.toString() |
| }); |
| details.requestHeaders.push({ |
| name: 'resourceType', |
| value: details.type |
| }); |
| return {requestHeaders: details.requestHeaders}; |
| }, |
| {urls: ['*://*/echoheader*']}, |
| [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )"; |
| std::string opt_extra_info_spec = "'blocking', 'requestHeaders'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kBackgroundScript, opt_extra_info_spec.c_str())); |
| |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| void RegisterServiceWorker(const std::string& worker_path, |
| const std::optional<std::string>& scope) { |
| auto* web_contents = GetActiveWebContents(); |
| GURL url = embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| std::string script = content::JsReplace("register($1, $2);", worker_path, |
| scope ? *scope : std::string()); |
| EXPECT_EQ("DONE", EvalJs(web_contents, script)); |
| } |
| |
| // Ensures requests made by the |worker_script_name| service worker can be |
| // intercepted by extensions. |
| void RunServiceWorkerFetchTest(const std::string& worker_script_name) { |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Install the test extension. |
| InstallRequestHeaderModifyingExtension(); |
| |
| // Register a service worker and navigate to a page it controls. |
| RegisterServiceWorker(worker_script_name, std::nullopt); |
| auto* web_contents = GetActiveWebContents(); |
| EXPECT_TRUE(NavigateToURL(web_contents, |
| embedded_test_server()->GetURL( |
| "/service_worker/fetch_from_page.html"))); |
| |
| // Make a fetch from the controlled page. Depending on the worker script, |
| // the fetch might go to the service worker and be re-issued, or might |
| // fallback to network, or skip the worker, etc. In any case, this function |
| // expects a network request to happen, and that the extension modify the |
| // headers of the request before it goes to network. Verify that it was able |
| // to inject a header of "foo=bar". |
| EXPECT_EQ("bar", |
| EvalJs(web_contents, "fetch_from_page('/echoheader?foo');")); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackgroundWithExtraHeaders, |
| ServiceWorkerWebRequestApiTest, |
| ::testing::Values(SWTestParams(true, ContextType::kPersistentBackground))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| ServiceWorkerWebRequestApiTest, |
| ::testing::Values(SWTestParams(false, ContextType::kPersistentBackground))); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorkerWithExtraHeaders, |
| ServiceWorkerWebRequestApiTest, |
| ::testing::Values(SWTestParams(true, ContextType::kServiceWorkerMV2))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| ServiceWorkerWebRequestApiTest, |
| ::testing::Values(SWTestParams(false, ContextType::kServiceWorkerMV2))); |
| |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, ServiceWorkerFetch) { |
| RunServiceWorkerFetchTest("fetch_event_respond_with_fetch.js"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, ServiceWorkerFallback) { |
| RunServiceWorkerFetchTest("fetch_event_pass_through.js"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, |
| ServiceWorkerNoFetchHandler) { |
| RunServiceWorkerFetchTest("empty.js"); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, |
| ServiceWorkerFallbackAfterRedirect) { |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| InstallRequestHeaderModifyingExtension(); |
| |
| RegisterServiceWorker("/fetch_event_passthrough.js", "/echoheader"); |
| |
| // Make sure the request is intercepted with no redirect. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| embedded_test_server()->GetURL("/echoheader?foo"))); |
| EXPECT_EQ("bar", EvalJs(web_contents, "document.body.textContent;")); |
| |
| // Make sure the request is intercepted with a redirect. |
| GURL redirect_url = embedded_test_server()->GetURL( |
| "/server-redirect?" + |
| embedded_test_server()->GetURL("/echoheader?foo").spec()); |
| ASSERT_TRUE(NavigateToURL(web_contents, redirect_url)); |
| EXPECT_EQ("bar", EvalJs(web_contents, "document.body.textContent;")); |
| } |
| |
| // An extension should be able to modify the request header for service worker |
| // script by using WebRequest API. |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, ServiceWorkerScript) { |
| // The extension to be used in this test adds foo=bar request header. |
| const char kScriptPath[] = "/echoheader_service_worker.js"; |
| // The request handler below will run on the EmbeddedTestServer's IO thread. |
| // Hence guard access to |served_service_worker_count| and |foo_header_value| |
| // using a lock. |
| base::Lock lock; |
| int served_service_worker_count = 0; |
| std::string foo_header_value; |
| |
| // Capture the value of a request header foo, which should be added if |
| // extension modifies the request header. |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url != kScriptPath) { |
| return nullptr; |
| } |
| |
| base::AutoLock auto_lock(lock); |
| ++served_service_worker_count; |
| foo_header_value.clear(); |
| if (request.headers.find("foo") != request.headers.end()) { |
| foo_header_value = request.headers.at("foo"); |
| } |
| |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("text/javascript"); |
| response->AddCustomHeader("Cache-Control", "no-cache"); |
| response->set_content("// empty"); |
| return response; |
| })); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| InstallRequestHeaderModifyingExtension(); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| GURL url = embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Register a service worker. The worker script should have "foo: bar" request |
| // header added by the extension. |
| std::string script = |
| content::JsReplace("register($1, './in-scope');", kScriptPath); |
| EXPECT_EQ("DONE", EvalJs(web_contents, script)); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(1, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| |
| // Update the worker. The worker should have "foo: bar" request header in the |
| // request for update checking. |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ("DONE", EvalJs(web_contents, "update('./in-scope');")); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(2, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| } |
| |
| // An extension should be able to modify the request header for module service |
| // worker script by using WebRequest API. |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, |
| ModuleServiceWorkerScript) { |
| // The extension to be used in this test adds foo=bar request header. |
| constexpr char kScriptPath[] = "/echoheader_service_worker.js"; |
| // The request handler below will run on the EmbeddedTestServer's IO thread. |
| // Hence guard access to |served_service_worker_count| and |foo_header_value| |
| // using a lock. |
| base::Lock lock; |
| int served_service_worker_count = 0; |
| std::string foo_header_value; |
| |
| // Capture the value of a request header foo, which should be added if |
| // extension modifies the request header. |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url != kScriptPath) { |
| return nullptr; |
| } |
| |
| base::AutoLock auto_lock(lock); |
| ++served_service_worker_count; |
| foo_header_value.clear(); |
| if (base::Contains(request.headers, "foo")) { |
| foo_header_value = request.headers.at("foo"); |
| } |
| |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("text/javascript"); |
| response->AddCustomHeader("Cache-Control", "no-cache"); |
| response->set_content("// empty"); |
| return response; |
| })); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| InstallRequestHeaderModifyingExtension(); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| GURL url = embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Register a service worker. `EvalJs` is blocked until the request handler |
| // serves the worker script. The worker script should have "foo: bar" request |
| // header added by the extension. |
| std::string script = |
| content::JsReplace("register($1, './in-scope', 'module');", kScriptPath); |
| EXPECT_EQ("DONE", EvalJs(web_contents, script)); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(1, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| |
| // Update the worker. `EvalJs` is blocked until the request handler serves the |
| // worker script. The worker should have "foo: bar" request header in the |
| // request for update checking. |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ("DONE", EvalJs(web_contents, "update('./in-scope');")); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(2, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| } |
| |
| // An extension should be able to modify the request header for module service |
| // worker script with static import by using WebRequest API. |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, |
| ModuleServiceWorkerScriptWithStaticImport) { |
| // The extension to be used in this test adds foo=bar request header. |
| constexpr char kScriptPath[] = "/static-import-worker.js"; |
| constexpr char kImportedScriptPath[] = "/echoheader_service_worker.js"; |
| // The request handler below will run on the EmbeddedTestServer's IO thread. |
| // Hence guard access to |served_service_worker_count| and |foo_header_value| |
| // using a lock. |
| base::Lock lock; |
| int served_service_worker_count = 0; |
| std::string foo_header_value; |
| |
| // Capture the value of a request header foo, which should be added if |
| // extension modifies the request header. |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| // Handle the top-level worker script. |
| if (request.relative_url == kScriptPath) { |
| base::AutoLock auto_lock(lock); |
| ++served_service_worker_count; |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("text/javascript"); |
| response->AddCustomHeader("Cache-Control", "no-cache"); |
| response->set_content("import './echoheader_service_worker.js';"); |
| return response; |
| } |
| // Handle the static-imported script. |
| if (request.relative_url == kImportedScriptPath) { |
| base::AutoLock auto_lock(lock); |
| ++served_service_worker_count; |
| foo_header_value.clear(); |
| if (base::Contains(request.headers, "foo")) { |
| foo_header_value = request.headers.at("foo"); |
| } |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("text/javascript"); |
| response->AddCustomHeader("Cache-Control", "no-cache"); |
| response->set_content("// empty"); |
| return response; |
| } |
| return nullptr; |
| })); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| InstallRequestHeaderModifyingExtension(); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| GURL url = embedded_test_server()->GetURL( |
| "/service_worker/create_service_worker.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Register a service worker. The worker script should have "foo: bar" request |
| // header added by the extension. |
| std::string script = |
| content::JsReplace("register($1, './in-scope', 'module');", kScriptPath); |
| EXPECT_EQ("DONE", EvalJs(web_contents, script)); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(2, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| |
| // Update the worker. The worker should have "foo: bar" request header in the |
| // request for update checking. |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ("DONE", EvalJs(web_contents, "update('./in-scope');")); |
| { |
| base::AutoLock auto_lock(lock); |
| EXPECT_EQ(4, served_service_worker_count); |
| EXPECT_EQ("bar", foo_header_value); |
| } |
| } |
| |
| // Ensure that extensions can intercept service worker navigation preload |
| // requests. |
| IN_PROC_BROWSER_TEST_P(ServiceWorkerWebRequestApiTest, |
| ServiceWorkerNavigationPreload) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Install the test extension. |
| InstallRequestHeaderModifyingExtension(); |
| |
| // Register a service worker that uses navigation preload. |
| RegisterServiceWorker("/service_worker/navigation_preload_worker.js", |
| "/echoheader"); |
| |
| // Navigate to "/echoheader". The browser will detect that the service worker |
| // above is registered with this scope and has navigation preload enabled. |
| // So it will send the navigation preload request to network while at the same |
| // time starting up the service worker. The service worker will get the |
| // response for the navigation preload request, and respond with it to create |
| // the page. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| GURL url = embedded_test_server()->GetURL( |
| "/echoheader?frameId&resourceType&service-worker-navigation-preload"); |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| |
| // Since the request was to "/echoheader", the response describes the request |
| // headers. |
| // |
| // The expectation is "0\nmain_frame\ntrue" because... |
| // |
| // 1) The extension is expected to add a "frameId: {id}" header, where {id} is |
| // details.frameId. This id is 0 for the main frame. |
| // 2) The extension is similarly expected to add a "resourceType: {type}" |
| // header, where {type} is details.type. |
| // 3) The browser adds a "service-worker-navigation-preload: true" header for |
| // navigation preload requests, so also sanity check that header to prove |
| // that this test is really testing the navigation preload request. |
| EXPECT_EQ("0\nmain_frame\ntrue", |
| EvalJs(web_contents, "document.body.textContent;")); |
| |
| // Repeat the test from an iframe, to test that details.frameId and resource |
| // type is populated correctly. |
| const char kAddIframe[] = R"( |
| (async () => { |
| const iframe = document.createElement('iframe'); |
| await new Promise(resolve => { |
| iframe.src = $1; |
| iframe.onload = resolve; |
| document.body.appendChild(iframe); |
| }); |
| const result = iframe.contentWindow.document.body.textContent; |
| |
| // Expect "{frameId}\nsub_frame\ntrue" where {frameId} is a positive |
| // integer. |
| const split = result.split('\n'); |
| if (parseInt(split[0]) > 0 && split[1] == 'sub_frame' && |
| split[2] == 'true') { |
| return 'ok'; |
| } |
| return 'bad result: ' + result; |
| })(); |
| )"; |
| |
| EXPECT_EQ("ok", EvalJs(web_contents, content::JsReplace(kAddIframe, url))); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Ensure we don't strip off initiator incorrectly in web request events when |
| // both the normal and incognito contexts are active. Regression test for |
| // crbug.com/934398. |
| // TODO(crbug.com/41493389): enable this flaky test |
| // Both Initiator Incognito tests build but don't run on desktop android. |
| #if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER) && defined(LEAK_SANITIZER) |
| #define MAYBE_Initiator_SpanningIncognito DISABLED_Initiator_SpanningIncognito |
| #else |
| #define MAYBE_Initiator_SpanningIncognito Initiator_SpanningIncognito |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_Initiator_SpanningIncognito) { |
| embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| ExtensionTestMessageListener ready_listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest") |
| .AppendASCII("initiator_spanning")); |
| ASSERT_TRUE(extension); |
| // Save the ID because enabling the extension for incognito mode will |
| // invalidate |extension|. |
| const ExtensionId extension_id = extension->id(); |
| EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| ASSERT_TRUE(PlatformOpenURLOffTheRecord(profile(), GURL("about:blank"))); |
| |
| // iframe.html loads an iframe to title1.html. The extension listens for the |
| // request to title1.html and records the request initiator. |
| const GURL url = embedded_test_server()->GetURL("google.com", "/iframe.html"); |
| const std::string url_origin = url::Origin::Create(url).Serialize(); |
| |
| static constexpr char kScript[] = R"( |
| chrome.test.sendScriptResult(JSON.stringify(self.initiators)); |
| self.initiators = []; |
| )"; |
| |
| // TODO(crbug.com/434990953): Verify that the active web contents is correct. |
| // On Win/Mac/Linux it's not the incognito web contents but on Android it is. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, url)); |
| std::optional<std::string> result = |
| ExecuteScriptAndReturnString(extension_id, profile(), kScript); |
| ASSERT_TRUE(result); |
| EXPECT_EQ(base::StringPrintf("[\"%s\"]", url_origin.c_str()), *result); |
| |
| // The extension isn't enabled in incognito. Se we shouldn't intercept the |
| // request to |url|. |
| ASSERT_TRUE(PlatformOpenURLOffTheRecord(profile(), url)); |
| result = ExecuteScriptAndReturnString(extension_id, profile(), kScript); |
| ASSERT_TRUE(result); |
| EXPECT_EQ("[]", *result); |
| |
| // Now enable the extension in incognito. |
| extension = nullptr; |
| ready_listener.Reset(); |
| extensions::util::SetIsIncognitoEnabled(extension_id, profile(), |
| true /* enabled */); |
| EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| // Now we should be able to intercept the incognito request. |
| ASSERT_TRUE(PlatformOpenURLOffTheRecord(profile(), url)); |
| result = ExecuteScriptAndReturnString(extension_id, profile(), kScript); |
| ASSERT_TRUE(result); |
| EXPECT_EQ(base::StringPrintf("[\"%s\"]", url_origin.c_str()), *result); |
| } |
| |
| // Ensure we don't strip off initiator incorrectly in web request events when |
| // both the normal and incognito contexts are active. Regression test for |
| // crbug.com/934398. |
| // Flaky on Linux. See http://crbug.com/1423252 |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_Initiator_SplitIncognito DISABLED_Initiator_SplitIncognito |
| #else |
| #define MAYBE_Initiator_SplitIncognito Initiator_SplitIncognito |
| #endif |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| MAYBE_Initiator_SplitIncognito) { |
| embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| ExtensionTestMessageListener ready_listener("ready"); |
| ExtensionTestMessageListener incognito_ready_listener("incognito ready"); |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("webrequest").AppendASCII("initiator_split"), |
| {.allow_in_incognito = true}); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| PlatformOpenURLOffTheRecord(profile(), GURL("about:blank")); |
| EXPECT_TRUE(incognito_ready_listener.WaitUntilSatisfied()); |
| |
| // iframe.html loads an iframe to title1.html. The extension listens for the |
| // request to title1.html and records the request initiator. |
| GURL url_normal = |
| embedded_test_server()->GetURL("google.com", "/iframe.html"); |
| GURL url_incognito = |
| embedded_test_server()->GetURL("example.com", "/iframe.html"); |
| const std::string origin_normal = url::Origin::Create(url_normal).Serialize(); |
| const std::string origin_incognito = |
| url::Origin::Create(url_incognito).Serialize(); |
| |
| static constexpr char kScript[] = R"( |
| chrome.test.sendScriptResult(JSON.stringify(self.initiators)); |
| self.initiators = []; |
| )"; |
| |
| // TODO(crbug.com/434990953): Verify that the active web contents is correct. |
| // On Win/Mac/Linux it's not the incognito web contents but on Android it is. |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url_normal)); |
| ASSERT_TRUE(PlatformOpenURLOffTheRecord(profile(), url_incognito)); |
| std::optional<std::string> result = |
| ExecuteScriptAndReturnString(extension->id(), profile(), kScript); |
| ASSERT_TRUE(result); |
| EXPECT_EQ(base::StringPrintf("[\"%s\"]", origin_normal.c_str()), *result); |
| result = ExecuteScriptAndReturnString( |
| extension->id(), |
| profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true), kScript); |
| ASSERT_TRUE(result); |
| EXPECT_EQ(base::StringPrintf("[\"%s\"]", origin_incognito.c_str()), *result); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // A request handler that sets the Access-Control-Allow-Origin header. |
| std::unique_ptr<net::test_server::HttpResponse> HandleXHRRequest( |
| const net::test_server::HttpRequest& request) { |
| auto http_response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| http_response->AddCustomHeader("Access-Control-Allow-Origin", "*"); |
| return http_response; |
| } |
| |
| // Regression test for http://crbug.com/971206. The responseHeaders should still |
| // be present in onBeforeRedirect even for HSTS upgrade. |
| IN_PROC_BROWSER_TEST_P( |
| ExtensionWebRequestApiTestWithContextTypeForHstsTopLevelNavigationOnly, |
| ExtraHeadersWithHSTSUpgrade) { |
| net::EmbeddedTestServer https_test_server( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| https_test_server.RegisterRequestHandler( |
| base::BindRepeating(&HandleXHRRequest)); |
| ASSERT_TRUE(https_test_server.Start()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request HSTS Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onBeforeRedirect.addListener(function(details) { |
| self.headerCount = details.responseHeaders.length; |
| }, {urls: ['<all_urls>']}, |
| ['responseHeaders', 'extraHeaders']); |
| |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, https_test_server.GetURL("/echo"))); |
| |
| content::StoragePartition* partition = |
| profile()->GetDefaultStoragePartition(); |
| base::RunLoop run_loop; |
| partition->GetNetworkContext()->AddHSTS( |
| https_test_server.host_port_pair().host(), |
| base::Time::Now() + base::Days(100), true, run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| PerformXhrInFrame(web_contents->GetPrimaryMainFrame(), |
| https_test_server.host_port_pair().host(), |
| https_test_server.port(), "echo"); |
| EXPECT_GT( |
| GetCountFromBackgroundScript(extension, profile(), "self.headerCount"), |
| 0); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Depends on declarativeWebRequest. crbug.com/332512510. |
| // Ensure that when an extension blocks a main-frame request, the resultant |
| // error page attributes this to an extension. |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, |
| ErrorPageForBlockedMainFrameNavigation) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest", {.extension_url = "test_simple_cancel_navigation.html"})) |
| << message_; |
| |
| WebContents* tab = GetActiveWebContents(); |
| std::string body = |
| content::EvalJs(tab, "document.body.textContent").ExtractString(); |
| |
| EXPECT_TRUE( |
| base::Contains(body, "This page has been blocked by an extension")); |
| EXPECT_TRUE(base::Contains(body, "Try disabling your extensions.")); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Regression test for https://crbug.com/1019614. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| HSTSUpgradeAfterRedirect) { |
| net::EmbeddedTestServer https_test_server( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(https_test_server.Start()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request HSTS Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onBeforeRedirect.addListener(() => {}, { |
| urls: [ '<all_urls>' ] |
| }, [ 'responseHeaders', 'extraHeaders' ]); |
| |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| content::StoragePartition* partition = |
| profile()->GetDefaultStoragePartition(); |
| base::test::TestFuture<void> hsts_added; |
| partition->GetNetworkContext()->AddHSTS("hsts.com", |
| base::Time::Now() + base::Days(100), |
| true, hsts_added.GetCallback()); |
| ASSERT_TRUE(hsts_added.Wait()); |
| |
| GURL final_url = https_test_server.GetURL("hsts.com", "/echo"); |
| GURL::Replacements replace_scheme; |
| replace_scheme.SetSchemeStr("http"); |
| GURL http_url = final_url.ReplaceComponents(replace_scheme); |
| GURL redirect_url = embedded_test_server()->GetURL( |
| "test.com", "/server-redirect?" + http_url.spec()); |
| |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_FALSE(NavigateToURL(web_contents, redirect_url)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| } |
| |
| // Regression test for https://crbug.com/40864513. This test passes if it |
| // doesn't crash. |
| // This is a copy of HSTSUpgradeAfterRedirect, but the redirect contains a CSP |
| // header. |
| // TODO(https://crbug.com/40864513) Enable this test. |
| IN_PROC_BROWSER_TEST_P(ExtensionWebRequestApiTestWithContextType, |
| DISABLED_HSTSUpgradeAfterRedirectWithCSP) { |
| net::EmbeddedTestServer https_test_server( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url.starts_with("/server-redirect-with-csp")) { |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->AddCustomHeader("Location", request.GetURL().query()); |
| response->AddCustomHeader("Content-Security-Policy", |
| "frame-ancestors 'none'"); |
| response->set_code(net::HTTP_MOVED_PERMANENTLY); |
| return response; |
| } |
| return nullptr; |
| })); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(https_test_server.Start()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request HSTS Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"( |
| chrome.webRequest.onBeforeRedirect.addListener(() => {}, { |
| urls: [ |
| '<all_urls>', |
| ] |
| }, [ |
| 'responseHeaders', |
| 'extraHeaders', |
| ]); |
| |
| chrome.test.sendMessage('ready'); |
| )"); |
| |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| content::StoragePartition* partition = |
| profile()->GetDefaultStoragePartition(); |
| base::test::TestFuture<void> hsts_added; |
| partition->GetNetworkContext()->AddHSTS("hsts.com", |
| base::Time::Now() + base::Days(100), |
| true, hsts_added.GetCallback()); |
| ASSERT_TRUE(hsts_added.Wait()); |
| |
| GURL final_url = https_test_server.GetURL("hsts.com", "/echo"); |
| GURL::Replacements replace_scheme; |
| replace_scheme.SetSchemeStr("http"); |
| GURL http_url = final_url.ReplaceComponents(replace_scheme); |
| GURL redirect_url = embedded_test_server()->GetURL( |
| "test.com", "/server-redirect-with-csp?" + http_url.spec()); |
| |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, redirect_url)); |
| EXPECT_EQ(final_url, web_contents->GetLastCommittedURL()); |
| } |
| |
| struct SWBTestParams { |
| // The parameter is for opt_extraInfoSpec passed to addEventListener. |
| // 'blocking' if it's false, and 'extraHeaders' in addition to them |
| // if it's true. |
| bool extra_info_spec; |
| ContextType context_type; |
| }; |
| |
| class SubresourceWebBundlesWebRequestApiTest |
| : public testing::WithParamInterface<SWBTestParams>, |
| public ExtensionApiTest { |
| public: |
| SubresourceWebBundlesWebRequestApiTest() |
| : ExtensionApiTest(GetParam().context_type) {} |
| ~SubresourceWebBundlesWebRequestApiTest() override = default; |
| SubresourceWebBundlesWebRequestApiTest( |
| const SubresourceWebBundlesWebRequestApiTest&) = delete; |
| SubresourceWebBundlesWebRequestApiTest& operator=( |
| const SubresourceWebBundlesWebRequestApiTest&) = delete; |
| |
| protected: |
| // Whether 'extraHeaders' is set in opt_extraInfoSpec of addEventListener. |
| enum class ExtraInfoSpec { |
| // No 'extraHeaders' |
| kDefault, |
| // with 'extraHeaders' |
| kExtraHeaders |
| }; |
| |
| static ExtraInfoSpec GetExtraInfoSpec() { |
| return GetParam().extra_info_spec ? ExtraInfoSpec::kExtraHeaders |
| : ExtraInfoSpec::kDefault; |
| } |
| |
| bool TryLoadScript(const std::string& script_src) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| std::string script = base::StringPrintf(R"( |
| (() => { |
| const script = document.createElement('script'); |
| return new Promise(resolve => { |
| script.addEventListener('load', () => { |
| resolve(true); |
| }); |
| script.addEventListener('error', () => { |
| resolve(false); |
| }); |
| script.src = '%s'; |
| document.body.appendChild(script); |
| }); |
| })(); |
| )", |
| script_src.c_str()); |
| return EvalJs(web_contents->GetPrimaryMainFrame(), script).ExtractBool(); |
| } |
| |
| bool TryLoadBundle(const std::string& href, const std::string& resources) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| std::string script = base::StringPrintf(R"( |
| (() => { |
| const script = document.createElement('script'); |
| script.type = 'webbundle'; |
| return new Promise(resolve => { |
| script.addEventListener('load', () => { |
| resolve(true); |
| }); |
| script.addEventListener('error', () => { |
| resolve(false); |
| }); |
| script.textContent = JSON.stringify({ |
| 'source': '%s', |
| 'resources': ['%s'] |
| }); |
| document.body.appendChild(script); |
| }); |
| })(); |
| )", |
| href.c_str(), resources.c_str()); |
| return EvalJs(web_contents->GetPrimaryMainFrame(), script).ExtractBool(); |
| } |
| |
| // Registers a request handler for static content. |
| void RegisterRequestHandler(const std::string& relative_url, |
| const std::string& content_type, |
| const std::string& content) { |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [relative_url, content_type, |
| content](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url == relative_url) { |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type(content_type); |
| response->set_content(content); |
| return std::move(response); |
| } |
| return nullptr; |
| })); |
| } |
| |
| // Registers a request handler for web bundle. This method takes a pointer of |
| // the content of the web bundle, because we can't create the content of the |
| // web bundle before starting the server since we need to know the port number |
| // of the test server due to the same-origin restriction (the origin of |
| // subresource which is written in the web bundle must be same as the origin |
| // of the web bundle), and we can't call |
| // EmbeddedTestServer::RegisterRequestHandler() after starting the server. |
| void RegisterWebBundleRequestHandler(const std::string& relative_url, |
| const std::string* web_bundle) { |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [relative_url, web_bundle](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| if (request.relative_url == relative_url) { |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("application/webbundle"); |
| response->AddCustomHeader("X-Content-Type-Options", "nosniff"); |
| response->set_content(*web_bundle); |
| return std::move(response); |
| } |
| return nullptr; |
| })); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackgroundWithExtraHeaders, |
| SubresourceWebBundlesWebRequestApiTest, |
| ::testing::Values(SWBTestParams(true, ContextType::kPersistentBackground))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| SubresourceWebBundlesWebRequestApiTest, |
| ::testing::Values(SWBTestParams(false, |
| ContextType::kPersistentBackground))); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorkerWithExtraHeaders, |
| SubresourceWebBundlesWebRequestApiTest, |
| ::testing::Values(SWBTestParams(true, ContextType::kServiceWorkerMV2))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| SubresourceWebBundlesWebRequestApiTest, |
| ::testing::Values(SWBTestParams(false, ContextType::kServiceWorkerMV2))); |
| |
| // Ensure web request listeners can intercept requests for a web bundle and its |
| // subresources. |
| // TODO(crbug.com/40801096): Fix flane and re-enable test. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| DISABLED_RequestIntercepted) { |
| const std::string uuid_in_package_script_url = |
| "uuid-in-package:6a059ece-62f4-4c79-a9e2-1c641cbdbaaf"; |
| // Create an extension that intercepts requests. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest"] |
| })"); |
| |
| std::string opt_extra_info_spec = ""; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += "'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| self.numMainResourceRequests = 0; |
| self.numWebBundleRequests = 0; |
| self.numScriptRequests = 0; |
| self.numUUIDInPackageScriptRequests = 0; |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| if (details.url.includes('test.html')) |
| self.numMainResourceRequests++; |
| else if (details.url.includes('web_bundle.wbn')) |
| self.numWebBundleRequests++; |
| else if (details.url.includes('test.js')) |
| self.numScriptRequests++; |
| else if (details.url === '%s') |
| self.numUUIDInPackageScriptRequests++; |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| uuid_in_package_script_url.c_str(), |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| const std::string page_html = base::StringPrintf( |
| R"( |
| <title>Loaded</title> |
| <body> |
| <script> |
| (() => { |
| const wbn_url = |
| new URL('./web_bundle.wbn', location.href).toString(); |
| const script_url = new URL('./test.js', location.href).toString(); |
| const uuid_in_package_script_url = '%s'; |
| |
| const script_web_bundle = document.createElement('script'); |
| script_web_bundle.type = 'webbundle'; |
| script_web_bundle.textContent = JSON.stringify({ |
| 'source': wbn_url, |
| 'resources': [script_url, uuid_in_package_script_url] |
| }); |
| document.body.appendChild(script); |
| const script = document.createElement('script'); |
| script.src = script_url; |
| script.addEventListener('load', () => { |
| const uuid_in_package_script = document.createElement('script'); |
| uuid_in_package_script.src = uuid_in_package_script_url; |
| document.body.appendChild(uuid_in_package_script); |
| }); |
| document.body.appendChild(script); |
| })(); |
| </script> |
| </body> |
| )", |
| uuid_in_package_script_url.c_str()); |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| RegisterRequestHandler("/test.html", "text/html", page_html); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| GURL script_url = embedded_test_server()->GetURL("/test.js"); |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| script_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'ScriptDone';"); |
| builder.AddExchange( |
| uuid_in_package_script_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title += ':UUIDInPackageScriptDone';"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| std::u16string expected_title = u"ScriptDone:UUIDInPackageScriptDone"; |
| content::TitleWatcher title_watcher(web_contents, expected_title); |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| // Check that the scripts in the web bundle are correctly loaded even when the |
| // extension intercepted the request. |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numMainResourceRequests")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numWebBundleRequests")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numScriptRequests")); |
| EXPECT_EQ( |
| 1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numUUIDInPackageScriptRequests")); |
| } |
| |
| // Ensure web request API can block the requests for the subresources inside the |
| // web bundle. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| RequestCanceled) { |
| // Create an extension that blocks a bundle subresource request. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string pass_uuid_in_package_js_url = |
| "uuid-in-package:bf50ad1f-899e-42ca-95ac-ca592aa2ecb5"; |
| std::string cancel_uuid_in_package_js_url = |
| "uuid-in-package:9cc02e52-05b6-466a-8c0e-f22ee86825a8"; |
| std::string opt_extra_info_spec = "'blocking'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| self.numPassScriptRequests = 0; |
| self.numCancelScriptRequests = 0; |
| self.numUUIDInPackagePassScriptRequests = 0; |
| self.numUUIDInPackageCancelScriptRequests = 0; |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| if (details.url.includes('pass.js')) { |
| self.numPassScriptRequests++; |
| return {cancel: false}; |
| } else if (details.url.includes('cancel.js')) { |
| self.numCancelScriptRequests++; |
| return {cancel: true}; |
| } else if (details.url === '%s') { |
| self.numUUIDInPackagePassScriptRequests++; |
| return {cancel: false}; |
| } else if (details.url === '%s') { |
| self.numUUIDInPackageCancelScriptRequests++; |
| return {cancel: true}; |
| } |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| pass_uuid_in_package_js_url.c_str(), |
| cancel_uuid_in_package_js_url.c_str(), |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| std::string page_html = base::StringPrintf( |
| R"( |
| <title>Loaded</title> |
| <body> |
| <script> |
| (() => { |
| const wbn_url = new URL('./web_bundle.wbn', location.href).toString(); |
| const pass_js_url = new URL('./pass.js', location.href).toString(); |
| const cancel_js_url = |
| new URL('./cancel.js', location.href).toString(); |
| const pass_uuid_in_package_js_url = '%s'; |
| const cancel_uuid_in_package_js_url = '%s'; |
| const script = document.createElement('script'); |
| script.type = 'webbundle'; |
| script.textContent = JSON.stringify({ |
| 'source': wbn_url, |
| 'resources': [pass_js_url, cancel_js_url, |
| pass_uuid_in_package_js_url, |
| cancel_uuid_in_package_js_url] |
| }); |
| document.body.appendChild(script); |
| })(); |
| </script> |
| </body> |
| )", |
| pass_uuid_in_package_js_url.c_str(), |
| cancel_uuid_in_package_js_url.c_str()); |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| RegisterRequestHandler("/test.html", "text/html", page_html); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| GURL pass_js_url = embedded_test_server()->GetURL("/pass.js"); |
| GURL cancel_js_url = embedded_test_server()->GetURL("/cancel.js"); |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| pass_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'script loaded';"); |
| builder.AddExchange( |
| cancel_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, ""); |
| builder.AddExchange( |
| pass_uuid_in_package_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'uuid-in-package script loaded';"); |
| builder.AddExchange( |
| cancel_uuid_in_package_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, ""); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| |
| std::u16string expected_title1 = u"script loaded"; |
| content::TitleWatcher title_watcher1(web_contents, expected_title1); |
| EXPECT_TRUE(TryLoadScript("pass.js")); |
| // Check that the script in the web bundle is correctly loaded even when the |
| // extension with blocking handler intercepted the request. |
| EXPECT_EQ(expected_title1, title_watcher1.WaitAndGetTitle()); |
| |
| EXPECT_FALSE(TryLoadScript("cancel.js")); |
| |
| std::u16string expected_title2 = u"uuid-in-package script loaded"; |
| content::TitleWatcher title_watcher2(web_contents, expected_title2); |
| EXPECT_TRUE(TryLoadScript(pass_uuid_in_package_js_url)); |
| // Check that the uuid-in-package script in the web bundle is correctly loaded |
| // even when the extension with blocking handler intercepted the request. |
| EXPECT_EQ(expected_title2, title_watcher2.WaitAndGetTitle()); |
| |
| EXPECT_FALSE(TryLoadScript(cancel_uuid_in_package_js_url)); |
| |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numPassScriptRequests")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numCancelScriptRequests")); |
| EXPECT_EQ( |
| 1, GetCountFromBackgroundScript( |
| extension, profile(), "self.numUUIDInPackagePassScriptRequests")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript( |
| extension, profile(), |
| "self.numUUIDInPackageCancelScriptRequests")); |
| } |
| |
| // Ensure web request API can change the headers of the subresources inside the |
| // web bundle. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, ChangeHeader) { |
| // Create an extension that changes the header of the subresources inside the |
| // web bundle. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string opt_extra_info_spec = "'blocking', 'responseHeaders'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| if (!details.url.includes('target.txt')) { |
| return {cancel: false}; |
| } |
| const headers = details.responseHeaders; |
| for (let i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'foo') { |
| headers[i].value += '-changed'; |
| } |
| } |
| headers.push({name: 'foo', value:'inserted'}); |
| return {responseHeaders: headers}; |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| const char kPageHtml[] = R"( |
| <title>Loaded</title> |
| <body> |
| <script> |
| (async () => { |
| const wbn_url = new URL('./web_bundle.wbn', location.href).toString(); |
| const target_url = new URL('./target.txt', location.href).toString(); |
| const script = document.createElement('script'); |
| script.type = 'webbundle'; |
| script.textContent = JSON.stringify({ |
| 'source': wbn_url, |
| 'resources': [target_url] |
| }); |
| document.body.appendChild(script); |
| const res = await fetch(target_url); |
| document.title = res.status + ':' + res.headers.get('foo'); |
| })(); |
| </script> |
| </body> |
| )"; |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| RegisterRequestHandler("/test.html", "text/html", kPageHtml); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| GURL target_txt_url = embedded_test_server()->GetURL("/target.txt"); |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| target_txt_url, |
| {{":status", "200"}, {"content-type", "text/plain"}, {"foo", "bar"}}, |
| "Hello world"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| std::u16string expected_title = u"200:bar-changed, inserted"; |
| content::TitleWatcher title_watcher(GetActiveWebContents(), expected_title); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| // Ensure web request API can change the response headers of uuid-in-package: |
| // URL subresources inside the web bundle. |
| // Note: Currently we can't directly check the response headers of |
| // uuid-in-package: URL resources, because CORS requests are not allowed for |
| // such URLs. So we change the content-type header of a JavaScript file and |
| // monitor the error handler. Subresources in web bundles should be treated as |
| // if an artificial `X-Content-Type-Options: nosniff` header field is set. So |
| // when the content-type is not suitable for script execution, the script |
| // should fail to load. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| ChangeHeaderUuidInPackageUrlResource) { |
| std::string uuid_url = "uuid-in-package:71940cde-d20b-4fb5-b920-38a58a92c516"; |
| // Create an extension that changes the header of the subresources inside |
| // the web bundle. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string opt_extra_info_spec = "'blocking', 'responseHeaders'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| static constexpr char kJsTemplate[] = R"( |
| chrome.webRequest.onHeadersReceived.addListener(function(details) { |
| if (details.url != '%s') { |
| return; |
| } |
| const headers = details.responseHeaders; |
| for (let i = 0; i < headers.length; i++) { |
| if (headers[i].name.toLowerCase() == 'content-type') { |
| headers[i].value = 'unknown/type'; |
| } |
| } |
| return {responseHeaders: headers}; |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )"; |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kJsTemplate, uuid_url.c_str(), |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| static constexpr char kHtmlTemplate[] = R"( |
| <title>Loaded</title> |
| <body> |
| <script> |
| (async () => { |
| const wbn_url = new URL('./web_bundle.wbn', location.href).toString(); |
| const uuid_url = '%s'; |
| const script_web_bundle = document.createElement('script'); |
| script_web_bundle.type = 'webbundle'; |
| script_web_bundle.textContent = JSON.stringify({ |
| 'source': wbn_url, |
| 'resources': [uuid_url] |
| }); |
| const script = document.createElement('script'); |
| script.src = uuid_url; |
| script.addEventListener('error', () => { |
| document.title = 'failed to load'; |
| }); |
| document.body.appendChild(script); |
| })(); |
| </script> |
| </body> |
| )"; |
| std::string page_html = base::StringPrintf(kHtmlTemplate, uuid_url.c_str()); |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| RegisterRequestHandler("/test.html", "text/html", page_html); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| uuid_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'loaded';"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| std::u16string expected_title = u"failed to load"; |
| content::TitleWatcher title_watcher(GetActiveWebContents(), expected_title); |
| |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| |
| // Ensure web request API can redirect the requests for the subresources inside |
| // the web bundle. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| RequestRedirected) { |
| // Create an extension that redirects a bundle subresource request. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string opt_extra_info_spec = "'blocking'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| if (details.url.includes('redirect.js')) { |
| const redirectUrl = |
| details.url.replace('redirect.js', 'redirected.js'); |
| return {redirectUrl: redirectUrl}; |
| } else if (details.url.includes('redirect_to_unlisted.js')) { |
| const redirectUrl = |
| details.url.replace('redirect_to_unlisted.js', |
| 'redirected_to_unlisted.js'); |
| return {redirectUrl: redirectUrl}; |
| } else if (details.url.includes('redirect_to_server.js')) { |
| const redirectUrl = |
| details.url.replace('redirect_to_server.js', |
| 'redirected_to_server.js'); |
| return {redirectUrl: redirectUrl}; |
| } |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| const char kPageHtml[] = R"( |
| <title>Loaded</title> |
| <body> |
| <script> |
| (() => { |
| const wbn_url = new URL('./web_bundle.wbn', location.href).toString(); |
| const redirect_js_url = |
| new URL('./redirect.js', location.href).toString(); |
| const redirected_js_url = |
| new URL('./redirected.js', location.href).toString(); |
| const redirect_to_unlisted_js_url = |
| new URL('./redirect_to_unlisted.js', location.href).toString(); |
| const redirect_to_server = |
| new URL('./redirect_to_server.js', location.href).toString(); |
| const script = document.createElement('script'); |
| script.type = 'webbundle'; |
| script.textContent = JSON.stringify({ |
| 'source': wbn_url, |
| 'resources': [redirect_js_url, redirected_js_url, |
| redirect_to_unlisted_js_url, redirect_to_server] |
| }); |
| document.body.appendChild(script); |
| |
| })(); |
| </script> |
| </body> |
| )"; |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| RegisterRequestHandler("/test.html", "text/html", kPageHtml); |
| RegisterRequestHandler("/redirect_to_server.js", "application/javascript", |
| "document.title = 'redirect_to_server';"); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| GURL redirect_js_url = embedded_test_server()->GetURL("/redirect.js"); |
| GURL redirected_js_url = embedded_test_server()->GetURL("/redirected.js"); |
| GURL redirect_to_unlisted_js_url = |
| embedded_test_server()->GetURL("/redirect_to_unlisted.js"); |
| GURL redirected_to_unlisted_js_url = |
| embedded_test_server()->GetURL("/redirected_to_unlisted.js"); |
| GURL redirect_to_server_js_url = |
| embedded_test_server()->GetURL("/redirect_to_server.js"); |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| redirect_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'redirect';"); |
| builder.AddExchange( |
| redirected_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'redirected';"); |
| builder.AddExchange( |
| redirect_to_unlisted_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'redirect_to_unlisted';"); |
| builder.AddExchange( |
| redirected_to_unlisted_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'redirected_to_unlisted';"); |
| builder.AddExchange( |
| redirect_to_server_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'redirect_to_server';"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| { |
| std::u16string expected_title = u"redirected"; |
| content::TitleWatcher title_watcher(web_contents, expected_title); |
| EXPECT_TRUE(TryLoadScript("redirect.js")); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| { |
| // In the current implementation, extensions can redirect the request to |
| // the other resource in the web bundle even if the resource is not listed |
| // in the resources attribute. |
| std::u16string expected_title = u"redirected_to_unlisted"; |
| content::TitleWatcher title_watcher(web_contents, expected_title); |
| EXPECT_TRUE(TryLoadScript("redirect_to_unlisted.js")); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| } |
| // In the current implementation, extensions can't redirect the request to |
| // outside the web bundle. |
| EXPECT_FALSE(TryLoadScript("redirect_to_server.js")); |
| } |
| |
| // Ensure that request to Subresource WebBundle fails if it is redirected by web |
| // request API. |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| WebBundleRequestRedirected) { |
| // Create an extension that redirects "redirect.wbn" to "redirected.wbn". |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string opt_extra_info_spec = "'blocking'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| if (!details.url.includes('redirect.wbn')) |
| return; |
| const redirectUrl = |
| details.url.replace('redirect.wbn', 'redirected.wbn'); |
| return {redirectUrl}; |
| }, {urls: ['<all_urls>']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/redirect.wbn", &web_bundle); |
| RegisterWebBundleRequestHandler("/redirected.wbn", &web_bundle); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| std::string js_url_str = embedded_test_server()->GetURL("/script.js").spec(); |
| web_package::WebBundleBuilder builder; |
| builder.AddExchange( |
| js_url_str, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'script loaded';"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/empty.html"))); |
| |
| // In the current implementation, extensions can't redirect requests to |
| // Subresource WebBundles. |
| EXPECT_FALSE(TryLoadBundle("redirect.wbn", js_url_str)); |
| |
| // Without redirection, bundle load should succeed. |
| EXPECT_TRUE(TryLoadBundle("redirected.wbn", js_url_str)); |
| } |
| |
| // Ensure web request listener can intercept requests for web bundles with the |
| // resource type "webbundle". |
| IN_PROC_BROWSER_TEST_P(SubresourceWebBundlesWebRequestApiTest, |
| WebBundleRequestCanceledWithResourceType) { |
| // Create an extension that cancels 'webbundle' resource type request in |
| // chrome.webRequest.onBeforeRequest listener. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Web Request Subresource Web Bundles Test", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| std::string opt_extra_info_spec = "'blocking'"; |
| if (GetExtraInfoSpec() == ExtraInfoSpec::kExtraHeaders) { |
| opt_extra_info_spec += ", 'extraHeaders'"; |
| } |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| self.numOnBeforeRequestCalled = 0; |
| self.unexpectedRequests = []; |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| self.numOnBeforeRequestCalled++; |
| if (details.type != 'webbundle') { |
| self.unexpectedRequests.push(details); |
| } |
| return {cancel: true}; |
| }, {urls: ['<all_urls>'], types: ['webbundle']}, [%s]); |
| |
| chrome.test.sendMessage('ready'); |
| )", |
| opt_extra_info_spec.c_str())); |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("ready"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| std::string web_bundle; |
| RegisterWebBundleRequestHandler("/web_bundle.wbn", &web_bundle); |
| |
| std::string page_html = R"( |
| <title>Page loaded</title> |
| <body> |
| <script> |
| (() => { |
| const script = document.createElement('script'); |
| script.type = 'webbundle'; |
| script.textContent = JSON.stringify({ |
| 'source': 'web_bundle.wbn', |
| 'resources': ['cancel.js'] |
| }); |
| script.addEventListener('error', () => { |
| document.title = 'web_bundle.wbn loading canceled'; |
| }); |
| script.addEventListener('load', () => { |
| document.title = 'web_bundle.wbn loaded'; |
| }); |
| document.body.appendChild(script); |
| })(); |
| </script> |
| </body> |
| )"; |
| RegisterRequestHandler("/test.html", "text/html", page_html); |
| |
| std::string pass_js = "document.title = 'pass.js loaded';"; |
| RegisterRequestHandler("/pass.js", "application/javascript", pass_js); |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Create a web bundle. |
| web_package::WebBundleBuilder builder; |
| GURL cancel_js_url = embedded_test_server()->GetURL("/cancel.js"); |
| builder.AddExchange( |
| cancel_js_url, |
| {{":status", "200"}, {"content-type", "application/javascript"}}, |
| "document.title = 'cancel.js loaded';"); |
| std::vector<uint8_t> bundle = builder.CreateBundle(); |
| web_bundle = std::string(bundle.begin(), bundle.end()); |
| |
| GURL page_url = embedded_test_server()->GetURL("/test.html"); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, page_url)); |
| EXPECT_EQ(page_url, web_contents->GetLastCommittedURL()); |
| |
| std::u16string expected_title1 = u"web_bundle.wbn loading canceled"; |
| content::TitleWatcher title_watcher1(web_contents, expected_title1); |
| title_watcher1.AlsoWaitForTitle(u"web_bundle.wbn loaded"); |
| // Check that the request for web_bundle.wbn was correctly canceled. |
| EXPECT_EQ(expected_title1, title_watcher1.WaitAndGetTitle()); |
| |
| // Check that the onBeforeRequest listener is called for the 'webbundle' |
| // resource type request. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numOnBeforeRequestCalled")); |
| static constexpr char kScript[] = |
| "chrome.test.sendScriptResult(JSON.stringify(self.unexpectedRequests));"; |
| EXPECT_EQ("[]", |
| ExecuteScriptAndReturnString(extension->id(), profile(), kScript)); |
| |
| // Try 'script' resource type request to check that the onBeforeRequest |
| // listener is invoked only for a 'webbundle' resource type request. |
| std::u16string expected_title2 = u"pass.js loaded"; |
| content::TitleWatcher title_watcher2(web_contents, expected_title2); |
| EXPECT_TRUE(TryLoadScript("pass.js")); |
| // Check that the pass.js was correctly loaded. |
| EXPECT_EQ(expected_title2, title_watcher2.WaitAndGetTitle()); |
| |
| // Check that the onBeforeRequest listener is not called for the 'script' |
| // resource type request. |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "self.numOnBeforeRequestCalled")); |
| EXPECT_EQ("[]", |
| ExecuteScriptAndReturnString(extension->id(), profile(), kScript)); |
| } |
| |
| // TODO(crbug.com/40130781) When we implement variant matching of subresource |
| // web bundles, we should add test for request header modification. |
| |
| enum class RedirectType { |
| kOnBeforeRequest, |
| kOnHeadersReceived, |
| }; |
| |
| struct RITestParams { |
| RedirectType redirect_type; |
| ContextType context_type; |
| }; |
| |
| class RedirectInfoWebRequestApiTest |
| : public testing::WithParamInterface<RITestParams>, |
| public ExtensionApiTest { |
| public: |
| RedirectInfoWebRequestApiTest() : ExtensionApiTest(GetParam().context_type) { |
| // TODO(crbug.com/40248833): Use HTTPS URLs in tests to avoid having to |
| // disable this feature. |
| feature_list_.InitAndDisableFeature(features::kHttpsUpgrades); |
| } |
| |
| ~RedirectInfoWebRequestApiTest() override = default; |
| RedirectInfoWebRequestApiTest(const RedirectInfoWebRequestApiTest&) = delete; |
| RedirectInfoWebRequestApiTest& operator=( |
| const RedirectInfoWebRequestApiTest&) = delete; |
| |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| } |
| |
| void InstallRequestRedirectingExtension(const std::string& resource_type) { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(R"({ |
| "name": "Simple Redirect", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| chrome.webRequest.%s.addListener(function(details) { |
| if (details.type == '%s' && |
| details.url.includes('hello.html')) { |
| var redirectUrl = |
| details.url.replace('original.test', 'redirected.test'); |
| return {redirectUrl: redirectUrl}; |
| } |
| }, {urls: ['*://original.test/*']}, ['blocking']); |
| chrome.test.sendMessage('ready'); |
| )", |
| GetParam().redirect_type == |
| RedirectType::kOnBeforeRequest |
| ? "onBeforeRequest" |
| : "onHeadersReceived", |
| resource_type.c_str())); |
| ExtensionTestMessageListener listener("ready"); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| private: |
| TestExtensionDir test_dir_; |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackgroundOnBeforeRequest, |
| RedirectInfoWebRequestApiTest, |
| ::testing::Values(RITestParams(RedirectType::kOnBeforeRequest, |
| ContextType::kPersistentBackground))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackgroundOnHeadersReceived, |
| RedirectInfoWebRequestApiTest, |
| ::testing::Values(RITestParams(RedirectType::kOnHeadersReceived, |
| ContextType::kPersistentBackground))); |
| |
| // These tests use webRequestBlocking and/or declarativeWebRequest. |
| // See crbug.com/332512510. |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorkerOnBeforeRequest, |
| RedirectInfoWebRequestApiTest, |
| ::testing::Values(RITestParams(RedirectType::kOnBeforeRequest, |
| ContextType::kServiceWorkerMV2))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorkerOnHeadersReceived, |
| RedirectInfoWebRequestApiTest, |
| ::testing::Values(RITestParams(RedirectType::kOnHeadersReceived, |
| ContextType::kServiceWorkerMV2))); |
| |
| // Test that a main frame request redirected by an extension has the correct |
| // site_for_cookies and network_isolation_key parameters. |
| #if !BUILDFLAG(IS_ANDROID) |
| // flaky on android. |
| IN_PROC_BROWSER_TEST_P(RedirectInfoWebRequestApiTest, |
| VerifyRedirectInfoMainFrame) { |
| InstallRequestRedirectingExtension("main_frame"); |
| |
| content::URLLoaderMonitor monitor; |
| |
| // Navigate to the URL that should be redirected, and check that the extension |
| // redirects it. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| GURL url = embedded_test_server()->GetURL("original.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| GURL redirected_url = |
| embedded_test_server()->GetURL("redirected.test", "/hello.html"); |
| EXPECT_EQ(redirected_url, web_contents->GetLastCommittedURL()); |
| |
| // Check the parameters passed to the URLLoaderFactory. |
| std::optional<network::ResourceRequest> resource_request = |
| monitor.GetRequestInfo(redirected_url); |
| ASSERT_TRUE(resource_request.has_value()); |
| EXPECT_TRUE(resource_request->site_for_cookies.IsFirstParty(redirected_url)); |
| ASSERT_TRUE(resource_request->trusted_params); |
| url::Origin redirected_origin = url::Origin::Create(redirected_url); |
| EXPECT_TRUE( |
| resource_request->trusted_params->isolation_info.IsEqualForTesting( |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, redirected_origin, |
| redirected_origin, |
| net::SiteForCookies::FromOrigin(redirected_origin)))); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| // Test that a sub frame request redirected by an extension has the correct |
| // site_for_cookies and network_isolation_key parameters. |
| IN_PROC_BROWSER_TEST_P(RedirectInfoWebRequestApiTest, |
| VerifyBeforeRequestRedirectInfoSubFrame) { |
| InstallRequestRedirectingExtension("sub_frame"); |
| |
| content::URLLoaderMonitor monitor; |
| |
| // Navigate to page with an iframe that should be redirected, and check that |
| // the extension redirects it. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| GURL original_iframed_url = |
| embedded_test_server()->GetURL("original.test", "/hello.html"); |
| GURL page_with_iframe_url = embedded_test_server()->GetURL( |
| "somewhere-else.test", |
| net::test_server::GetFilePathWithReplacements( |
| "/page_with_iframe.html", |
| base::StringPairs{ |
| {"title1.html", original_iframed_url.spec().c_str()}})); |
| EXPECT_TRUE(NavigateToURL(web_contents, page_with_iframe_url)); |
| EXPECT_EQ(page_with_iframe_url, web_contents->GetLastCommittedURL()); |
| |
| content::RenderFrameHostWrapper child_frame( |
| ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0)); |
| ASSERT_TRUE(child_frame); |
| |
| GURL redirected_url = |
| embedded_test_server()->GetURL("redirected.test", "/hello.html"); |
| ASSERT_EQ(redirected_url, child_frame->GetLastCommittedURL()); |
| |
| // Check the parameters passed to the URLLoaderFactory. |
| std::optional<network::ResourceRequest> resource_request = |
| monitor.GetRequestInfo(redirected_url); |
| ASSERT_TRUE(resource_request.has_value()); |
| EXPECT_TRUE( |
| resource_request->site_for_cookies.IsFirstParty(page_with_iframe_url)); |
| EXPECT_FALSE(resource_request->site_for_cookies.IsFirstParty(redirected_url)); |
| ASSERT_TRUE(resource_request->trusted_params); |
| url::Origin top_level_origin = url::Origin::Create(page_with_iframe_url); |
| url::Origin redirected_origin = url::Origin::Create(redirected_url); |
| EXPECT_TRUE( |
| resource_request->trusted_params->isolation_info.IsEqualForTesting( |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kSubFrame, top_level_origin, |
| redirected_origin, |
| net::SiteForCookies::FromOrigin(top_level_origin), |
| /*nonce=*/std::nullopt, net::NetworkIsolationPartition::kGeneral, |
| net::IsolationInfo::FrameAncestorRelation::kSameOrigin))); |
| } |
| |
| // Regression test for crbug.com/1510422 to validate that redirection to an |
| // invalid URL by extension does not crash the browser. |
| IN_PROC_BROWSER_TEST_P(RedirectInfoWebRequestApiTest, |
| VerifyInvalidUrlRedirection) { |
| TestExtensionDir test_dir; |
| static constexpr char kInvalidUrl[] = "https://www.invalid.[ss]com/"; |
| test_dir.WriteManifest(R"({ |
| "name": "Simple Redirect", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { "scripts": ["background.js"], "persistent": true }, |
| "permissions": ["<all_urls>", "webRequest", "webRequestBlocking"] |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(R"( |
| chrome.webRequest.onBeforeRequest.addListener(function(details) { |
| if (details.url.includes('hello.html')) { |
| var redirectUrl = '%s'; |
| return {redirectUrl: redirectUrl}; |
| } |
| }, {urls: ['*://redirect.test/*']}, ['blocking']); |
| chrome.test.sendMessage('ready'); |
| )", |
| kInvalidUrl)); |
| |
| // Since we can't catch the error in the extension's JS, we instead listen to |
| // the error come into the error console. |
| ErrorConsoleTestObserver error_observer(2u, profile()); |
| error_observer.EnableErrorCollection(); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to the URL that should be redirected, and check that the extension |
| // navigation happens successfully. |
| auto* web_contents = GetActiveWebContents(); |
| content::TestNavigationObserver navigation_observer(web_contents); |
| GURL url = embedded_test_server()->GetURL("redirect.test", "/hello.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| |
| error_observer.WaitForErrors(); |
| const ErrorList& errors = |
| ErrorConsole::Get(profile())->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(2u, errors.size()); |
| EXPECT_EQ( |
| base::ASCIIToUTF16(base::StringPrintf( |
| "Unchecked runtime.lastError: redirectUrl '%s' is not a valid URL.", |
| kInvalidUrl)), |
| errors[1]->message()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Depends on LoginHandler, which doesn't work on android. |
| class ProxyCORSWebRequestApiTest |
| : public ExtensionApiTest, |
| public testing::WithParamInterface<ContextType> { |
| public: |
| ProxyCORSWebRequestApiTest() : ExtensionApiTest(GetParam()) {} |
| ~ProxyCORSWebRequestApiTest() override = default; |
| ProxyCORSWebRequestApiTest(const ProxyCORSWebRequestApiTest&) = delete; |
| ProxyCORSWebRequestApiTest& operator=(const ProxyCORSWebRequestApiTest&) = |
| delete; |
| |
| protected: |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| proxy_cors_server_.RegisterRequestHandler(base::BindRepeating( |
| &ProxyCORSWebRequestApiTest::HandleProxiedCORSRequest, |
| base::Unretained(this))); |
| ASSERT_TRUE(proxy_cors_server_.Start()); |
| |
| PrefService* pref_service = profile()->GetPrefs(); |
| pref_service->SetDict(proxy_config::prefs::kProxy, |
| ProxyConfigDictionary::CreateFixedServers( |
| proxy_cors_server_.host_port_pair().ToString(), |
| "accounts.google.com")); |
| |
| // Flush the proxy configuration change to avoid any races. |
| ProfileNetworkContextServiceFactory::GetForContext(profile()) |
| ->FlushProxyConfigMonitorForTesting(); |
| profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting(); |
| } |
| |
| std::unique_ptr<net::test_server::HttpResponse> HandleProxiedCORSRequest( |
| const net::test_server::HttpRequest& request) { |
| std::string request_url; |
| // Request url will be replaced by host:port pair of embedded proxy server |
| // in HttpRequest, extract requested url from request line instead. |
| std::vector<std::string> request_lines = |
| base::SplitString(request.all_headers, "\r\n", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (!request_lines.empty()) { |
| std::vector<std::string> request_line = |
| base::SplitString(request_lines[0], " ", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| if (request_line.size() > 1) { |
| request_url = request_line[1]; |
| } |
| } |
| if (request_url != kCORSUrl) { |
| return nullptr; |
| } |
| |
| // Handle request as proxy server. |
| const auto proxy_auth = request.headers.find("Proxy-Authorization"); |
| std::string auth; |
| if (proxy_auth != request.headers.end()) { |
| auth = proxy_auth->second; |
| const std::string auth_method_prefix = "Basic "; |
| const auto prefix_pos = auth.find(auth_method_prefix); |
| EXPECT_EQ(0U, prefix_pos); |
| EXPECT_GT(auth.size(), auth_method_prefix.size()); |
| if (prefix_pos == 0U && auth.size() > auth_method_prefix.size()) { |
| auth = auth.substr(auth_method_prefix.size()); |
| EXPECT_TRUE(base::Base64Decode(auth, &auth)); |
| } else { |
| auth.clear(); |
| } |
| } |
| if (auth != base::StringPrintf("%s:%s", kCORSProxyUser, kCORSProxyPass)) { |
| std::unique_ptr<net::test_server::BasicHttpResponse> response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->AddCustomHeader("Proxy-Authenticate", |
| "Basic realm=\"TestRealm\""); |
| response->set_code(net::HTTP_PROXY_AUTHENTICATION_REQUIRED); |
| return response; |
| } |
| |
| // Handle request as cors server. |
| if (request.method == net::test_server::METHOD_OPTIONS) { |
| const auto preflight_method = |
| request.headers.find("Access-Control-Request-Method"); |
| const auto preflight_header = |
| request.headers.find("Access-Control-Request-Headers"); |
| if (preflight_method == request.headers.end() || |
| preflight_header == request.headers.end()) { |
| ADD_FAILURE() << "Expected Access-Control-Request-* headers were not " |
| "found in preflight request"; |
| std::unique_ptr<net::test_server::BasicHttpResponse> response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_BAD_REQUEST); |
| return response; |
| } |
| EXPECT_EQ("GET", preflight_method->second); |
| EXPECT_EQ(kCustomPreflightHeader, preflight_header->second); |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->AddCustomHeader("Access-Control-Allow-Origin", "*"); |
| response->AddCustomHeader("Access-Control-Allow-Methods", "GET"); |
| response->AddCustomHeader("Access-Control-Allow-Headers", |
| kCustomPreflightHeader); |
| response->set_code(net::HTTP_NO_CONTENT); |
| if (preflight_waiter_) { |
| preflight_waiter_->Quit(); |
| } |
| return response; |
| } |
| EXPECT_EQ(net::test_server::METHOD_GET, request.method); |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->AddCustomHeader("Access-Control-Allow-Origin", "*"); |
| response->set_content_type("text/plain"); |
| response->set_content("PASS"); |
| response->set_code(net::HTTP_OK); |
| return response; |
| } |
| |
| // Executes a CORS preflight request on the main frame with a custom preflight |
| // header. |
| void ExecuteCorsPreflightedRequest() { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| static constexpr char kCORSPreflightedRequest[] = R"( |
| var xhr = new XMLHttpRequest(); |
| xhr.open('GET', '%s'); |
| xhr.setRequestHeader('%s', 'testvalue'); |
| new Promise(resolve => { |
| xhr.onload = () => { resolve(true); }; |
| xhr.onerror = () => { resolve(false); }; |
| xhr.send(); |
| }); |
| )"; |
| |
| ExecuteScriptAsyncWithoutUserGesture( |
| web_contents->GetPrimaryMainFrame(), |
| base::StringPrintf(kCORSPreflightedRequest, kCORSUrl, |
| kCustomPreflightHeader)); |
| } |
| |
| void WaitForPreflightResponse() { |
| DCHECK(preflight_waiter_); |
| preflight_waiter_->Run(); |
| preflight_waiter_.reset(); |
| } |
| |
| // A waiter for a preflight request to complete. |
| std::unique_ptr<base::RunLoop> preflight_waiter_; |
| |
| private: |
| net::EmbeddedTestServer proxy_cors_server_; |
| }; |
| |
| using ProxyCORSDeclarativeNetRequestApiTest = ProxyCORSWebRequestApiTest; |
| using ProxyCORSWebRequestApiTestWithContextTypeMv3 = ProxyCORSWebRequestApiTest; |
| |
| INSTANTIATE_TEST_SUITE_P(PersistentBackground, |
| ProxyCORSWebRequestApiTest, |
| ::testing::Values(ContextType::kPersistentBackground)); |
| |
| INSTANTIATE_TEST_SUITE_P(ServiceWorker, |
| ProxyCORSWebRequestApiTest, |
| ::testing::Values(ContextType::kServiceWorker)); |
| |
| INSTANTIATE_TEST_SUITE_P(/* No prefix */, |
| ProxyCORSDeclarativeNetRequestApiTest, |
| ::testing::Values(ContextType::kFromManifest)); |
| |
| INSTANTIATE_TEST_SUITE_P(/* No prefix */, |
| ProxyCORSWebRequestApiTestWithContextTypeMv3, |
| ::testing::Values(ContextType::kFromManifest)); |
| |
| // Regression test for crbug.com/1212625 |
| // Test that CORS preflight request which requires proxy auth completes |
| // successfully instead of being cancelled after proxy auth required response. |
| // This case requires an extension with a webRequest extraHeaders listener that |
| // matches the preflight request. |
| IN_PROC_BROWSER_TEST_P(ProxyCORSWebRequestApiTest, |
| PreflightCompletesSuccessfully) { |
| ExtensionTestMessageListener ready_listener("ready"); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_cors_preflight")); |
| ASSERT_TRUE(extension) << message_; |
| ASSERT_TRUE(ready_listener.WaitUntilSatisfied()); |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/empty.html"))); |
| |
| ExtensionTestMessageListener preflight_listener("cors-preflight-succeeded"); |
| |
| ExecuteCorsPreflightedRequest(); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| []() { return LoginHandler::GetAllLoginHandlersForTest().size() == 1; })); |
| |
| LoginHandler::GetAllLoginHandlersForTest().front()->SetAuth( |
| base::ASCIIToUTF16(std::string(kCORSProxyUser)), |
| base::ASCIIToUTF16(std::string(kCORSProxyPass))); |
| |
| EXPECT_TRUE(preflight_listener.WaitUntilSatisfied()); |
| EXPECT_EQ(1, GetCountFromBackgroundScript( |
| extension, profile(), "self.preflightHeadersReceivedCount")); |
| EXPECT_EQ( |
| 1, GetCountFromBackgroundScript(extension, profile(), |
| "self.preflightProxyAuthRequiredCount")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript( |
| extension, profile(), "self.preflightResponseStartedCount")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript( |
| extension, profile(), |
| "self.preflightResponseStartedSuccessfullyCount")); |
| } |
| |
| // Regression for crbug.com/369836605 |
| // Similar to the above test, except the "listener" in this case is a DNR rule |
| // that would require extraHeaders information but does NOT match on the |
| // preflight request. |
| IN_PROC_BROWSER_TEST_P(ProxyCORSDeclarativeNetRequestApiTest, |
| PreflightCompletesWithNonMatchingDNRRule) { |
| static constexpr char kManifest[] = R"({ |
| "name": "Simple DNR ModifyHeaders", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": [ "declarativeNetRequest" ], |
| "host_permissions": [ "https://nomatch.com/*" ], |
| "declarative_net_request" : { |
| "rule_resources" : [{ |
| "id": "ruleset", |
| "enabled": true, |
| "path": "rules.json" |
| }] |
| } |
| })"; |
| static constexpr char kRules[] = R"([{ |
| "id": 1, |
| "action": { |
| "requestHeaders": [ |
| { |
| "header": "referer", |
| "operation": "remove" |
| } |
| ], |
| "type": "modifyHeaders" |
| }, |
| "condition": { |
| "urlFilter": "nomatch.com", |
| "resourceTypes": ["main_frame"] |
| } |
| }])"; |
| |
| // Load an extension with one declarativeNetRequest modifyHeaders rule that |
| // removes the referer header but will not match any requests from this test. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("rules.json"), kRules); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| preflight_waiter_ = std::make_unique<base::RunLoop>(); |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/empty.html"))); |
| ExecuteCorsPreflightedRequest(); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| []() { return LoginHandler::GetAllLoginHandlersForTest().size() == 1; })); |
| |
| LoginHandler::GetAllLoginHandlersForTest().front()->SetAuth( |
| base::ASCIIToUTF16(std::string(kCORSProxyUser)), |
| base::ASCIIToUTF16(std::string(kCORSProxyPass))); |
| |
| // Wait for the proxy server to return a response for the preflight request |
| // after auth is provided. If the preflight request was cancelled, then this |
| // test will not finish. |
| WaitForPreflightResponse(); |
| } |
| |
| // Tests that an extension can successfully receive and handle an |
| // `onAuthRequired` event for a CORS preflighted request that requires proxy |
| // authentication. The test verifies that the extension's handler provides the |
| // necessary credentials and the preflight request completes successfully. |
| // Regression test for https://crbug.com/40928015. |
| IN_PROC_BROWSER_TEST_P(ProxyCORSWebRequestApiTestWithContextTypeMv3, |
| PreflightOnAuthRequiredSuccessful) { |
| static constexpr char kManifest[] = R"({ |
| "name": "MV3 CORS Preflight webRequest.OnAuthRequired", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": [ "webRequest", "webRequestAuthProvider" ], |
| "host_permissions": [ "http://127.0.0.1/*", "http://cors.test/*" ], |
| "background": { "service_worker": "background.js" } |
| })"; |
| |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onAuthRequired.addListener(details => { |
| const authCredentials = { username: '%s', password: '%s' }; |
| chrome.test.succeed(); |
| return {authCredentials}; |
| }, |
| { urls: ['<all_urls>'] }, |
| ['blocking']);)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kBackgroundJs, kCORSProxyUser, kCORSProxyPass)); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension) << message_; |
| |
| ResultCatcher result_catcher; |
| preflight_waiter_ = std::make_unique<base::RunLoop>(); |
| |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/empty.html"))); |
| // Send a CORS preflight request which requires proxy auth. |
| ExecuteCorsPreflightedRequest(); |
| |
| // The extension should have received the OnAuthRequired event. |
| EXPECT_TRUE(result_catcher.GetNextResult()); |
| // Wait for the proxy server to return a response for the preflight request |
| // after auth is provided. If the preflight request was cancelled, then this |
| // test will not finish. |
| WaitForPreflightResponse(); |
| } |
| |
| // Tests that an extension can successfully use 'extraHeaders' and |
| // 'declarativeNetRequest' (with header-dependent rules) while handling |
| // 'onAuthRequired' for a CORS preflight request. |
| // Regression test for crbug.com/444248440. |
| IN_PROC_BROWSER_TEST_P(ProxyCORSWebRequestApiTestWithContextTypeMv3, |
| PreflightOnAuthRequiredWithExtraHeadersDNR) { |
| static constexpr char kManifest[] = R"({ |
| "name": "MV3 CORS Preflight webRequest.OnAuthRequired extraHeaders DNR", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": [ |
| "webRequest", |
| "webRequestAuthProvider", |
| "declarativeNetRequest" |
| ], |
| "host_permissions": [ "http://127.0.0.1/*", "http://cors.test/*" ], |
| "background": { "service_worker": "background.js" }, |
| "declarative_net_request": { |
| "rule_resources": [{ |
| "id": "ruleset_1", |
| "enabled": true, |
| "path": "rules.json" |
| }] |
| } |
| })"; |
| |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onAuthRequired.addListener(details => { |
| const authCredentials = { username: '%s', password: '%s' }; |
| setTimeout(() => { |
| chrome.test.succeed(); |
| }, 0); |
| return {authCredentials}; |
| }, |
| { urls: ['<all_urls>'] }, |
| // 'extraHeaders' is necessary to hit the critical path. |
| ['blocking', 'extraHeaders']);)"; |
| |
| // Use a DNR rule that depends on response headers. By including |
| // 'responseHeaders' in the condition, the rule cannot be evaluated until |
| // headers are received. |
| // This ensures `RulesetManager::HasRulesets(kOnHeadersReceived)` returns |
| // true, forcing the call to `EvaluateRequestWithHeaders`. |
| static constexpr char kRulesJson[] = R"([ |
| { |
| "id": 1, |
| "priority": 1, |
| "action": { "type": "block" }, |
| "condition": { |
| "urlFilter": "*", |
| "responseHeaders": [{"header": "header-that-forces-evaluation"}] |
| } |
| } |
| ])"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| base::StringPrintf(kBackgroundJs, kCORSProxyUser, kCORSProxyPass)); |
| test_dir.WriteFile(FILE_PATH_LITERAL("rules.json"), kRulesJson); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension) << message_; |
| |
| ResultCatcher result_catcher; |
| preflight_waiter_ = std::make_unique<base::RunLoop>(); |
| |
| ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL("/empty.html"))); |
| |
| // Send a CORS preflight request which requires proxy auth. |
| // This triggers the sequence: |
| // - Preflight |
| // - Auth Challenge |
| // - HandleAuthRequest (with extraHeaders) |
| // - HandleResponseOrRedirectHeaders |
| // - WebRequestEventRouter::OnHeadersReceived |
| // - RulesetManager::HasRulesets(kOnHeadersReceived) == true |
| // - RulesetManager::EvaluateRequestWithHeaders(nullptr) |
| // - Graceful handling of headers == nullptr. |
| ExecuteCorsPreflightedRequest(); |
| |
| EXPECT_TRUE(result_catcher.GetNextResult()); |
| WaitForPreflightResponse(); |
| } |
| |
| // Depends on declarativeWebRequest. crbug.com/332512510. |
| class ExtensionWebRequestApiFencedFrameTest |
| : public ExtensionWebRequestApiTest { |
| protected: |
| ExtensionWebRequestApiFencedFrameTest() { |
| feature_list_.InitWithFeaturesAndParameters( |
| {{blink::features::kFencedFrames, {}}, |
| {blink::features::kFencedFramesAPIChanges, {}}, |
| {blink::features::kFencedFramesDefaultMode, {}}, |
| {features::kPrivacySandboxAdsAPIsOverride, {}}, |
| {blink::features::kFencedFramesLocalUnpartitionedDataAccess, {}}}, |
| {/* disabled_features */}); |
| // Fenced frames are only allowed in secure contexts. |
| UseHttpsTestServer(); |
| } |
| ~ExtensionWebRequestApiFencedFrameTest() override = default; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiFencedFrameTest, Load) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest", |
| {.extension_url = "test_fenced_frames.html"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiFencedFrameTest, |
| DeclarativeSendMessage) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest", {.extension_url = "test_fenced_frames_send_message.html"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiFencedFrameTest, |
| NetworkRevocation) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest", |
| {.extension_url = "test_fenced_frames_network_revocation.html"})) |
| << message_; |
| } |
| |
| // Depends on declarativeWebRequest. crbug.com/332512510. |
| class ExtensionWebRequestApiPrerenderingTest |
| : public ExtensionWebRequestApiTest { |
| protected: |
| ExtensionWebRequestApiPrerenderingTest() = default; |
| ~ExtensionWebRequestApiPrerenderingTest() override = default; |
| |
| private: |
| content::test::ScopedPrerenderFeatureList prerender_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiPrerenderingTest, Load) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webrequest", |
| {.extension_url = "test_prerendering.html"})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiPrerenderingTest, LoadIntoNewTab) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest( |
| "webrequest", {.extension_url = "test_prerendering_into_new_tab.html"})) |
| << message_; |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // A clunky test suite class to allow for waiting for a message to be sent from |
| // the extension's background context when it starts up. We need this because |
| // we don't currently have a good way of waiting for a service worker context to |
| // be fully initialized. |
| class WebRequestPersistentListenersTest |
| : public ExtensionWebRequestApiTestWithContextType { |
| public: |
| WebRequestPersistentListenersTest() |
| // Note: Set the listener before triggering the parent |
| // SetUpOnMainThread to ensure it happens before extensions start |
| // loading. |
| : test_listener_( |
| std::make_unique<ExtensionTestMessageListener>("ready")) {} |
| ~WebRequestPersistentListenersTest() override = default; |
| |
| void TearDownOnMainThread() override { |
| test_listener_.reset(); |
| ExtensionWebRequestApiTestWithContextType::TearDownOnMainThread(); |
| } |
| |
| void WaitForReadyMessage() { |
| EXPECT_TRUE(test_listener_->WaitUntilSatisfied()); |
| } |
| |
| private: |
| std::unique_ptr<ExtensionTestMessageListener> test_listener_; |
| }; |
| |
| // Tests that webRequest listeners are persistent across browser restarts. |
| IN_PROC_BROWSER_TEST_P(WebRequestPersistentListenersTest, |
| PRE_TestListenersArePersistent) { |
| // Load an extension that listens for webRequest events. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("webrequest_persistent")); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to example.com (a site the extension has access to). |
| ASSERT_TRUE(NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL("example.com", "/simple.html"))); |
| |
| // Validate that we have a single request seen by the extension. |
| base::Value request_count = BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), kGetNumRequests, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| ASSERT_TRUE(request_count.is_int()); |
| EXPECT_EQ(1, request_count.GetInt()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebRequestPersistentListenersTest, |
| TestListenersArePersistent) { |
| // Find the installed extension and wait for it to fully load. |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| const Extension* extension = nullptr; |
| for (const auto& candidate : extension_registry()->enabled_extensions()) { |
| if (candidate->name() == "Web Request Persistence") { |
| extension = candidate.get(); |
| break; |
| } |
| } |
| ASSERT_TRUE(extension); |
| WaitForExtensionViewsToLoad(); |
| WaitForReadyMessage(); |
| |
| // Navigate once more to example.com. |
| ASSERT_TRUE(NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL("example.com", "/simple.html"))); |
| |
| // We should now have two records seen by the extension. |
| base::Value request_count = BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), kGetNumRequests, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| |
| ASSERT_TRUE(request_count.is_int()); |
| EXPECT_EQ(2, request_count.GetInt()); |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Android only supports manifest V3 / service worker. |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| WebRequestPersistentListenersTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kPersistentBackground, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| WebRequestPersistentListenersTest, |
| ::testing::Values( |
| std::make_pair( |
| ContextType::kServiceWorker, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled), |
| std::make_pair( |
| ContextType::kServiceWorker, |
| BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled)), |
| ExtensionWebRequestApiTestWithContextType::PrintToStringParamName()); |
| |
| class ManifestV3WebRequestApiTest : public ExtensionWebRequestApiTest { |
| public: |
| ManifestV3WebRequestApiTest() = default; |
| ~ManifestV3WebRequestApiTest() override = default; |
| |
| // Loads an extension contained within `test_dir` as a policy-installed |
| // extension. This is useful because webRequestBlocking is restricted to |
| // policy-installed extensions in Manifest V3. |
| // This assumes the extension script will send a "ready" message once it's |
| // done setting up. |
| const Extension* LoadPolicyExtension(TestExtensionDir& test_dir) { |
| // We need a "ready"-style listener here because `InstallExtension()` |
| // doesn't automagically wait for the extension to finish setting up. |
| ExtensionTestMessageListener listener("ready"); |
| // Since we may programmatically stop the worker, we also need to wait for |
| // the registration to be fully stored. |
| service_worker_test_utils::TestServiceWorkerContextObserver |
| registration_observer(profile()); |
| base::FilePath packed_path = test_dir.Pack(); |
| const Extension* extension = InstallExtension( |
| packed_path, 1, mojom::ManifestLocation::kExternalPolicyDownload); |
| EXPECT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| registration_observer.WaitForRegistrationStored(); |
| |
| return extension; |
| } |
| |
| WebRequestEventRouter* web_request_router() { |
| return WebRequestEventRouter::Get(profile()); |
| } |
| |
| std::optional<WorkerId> GetWorkerIdForExtension( |
| const ExtensionId& extension_id) { |
| std::vector<WorkerId> service_workers_for_extension = |
| ProcessManager::Get(profile())->GetServiceWorkersForExtension( |
| extension_id); |
| if (service_workers_for_extension.size() > 1u) { |
| ADD_FAILURE() << "Expected only one worker for extension: " |
| << extension_id |
| << " But found incorrect number of workers: " |
| << service_workers_for_extension.size(); |
| return std::nullopt; |
| } |
| return service_workers_for_extension.empty() |
| ? std::nullopt |
| : std::optional<WorkerId>(service_workers_for_extension[0]); |
| } |
| }; |
| |
| // Tests a service worker-based extension intercepting requests with |
| // webRequestBlocking. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, WebRequestBlocking) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://block.example/*", |
| "http://allow.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that cancels any requests that include |
| // block.example. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| if (details.url.includes('block.example')) { |
| return {cancel: true} |
| } |
| return {}; |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| // Navigate to allow.example. This should succeed. |
| { |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("allow.example", "/simple.html"))); |
| EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); |
| } |
| |
| // Now, navigate to block.example. This navigation should be blocked. |
| { |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_FALSE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("block.example", "/simple.html"))); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code()); |
| } |
| } |
| |
| // Tests an extension returning a promise from a webRequest blocking handler to |
| // deliver an async response. This is only available to policy-installed |
| // extensions. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| WebRequestBlockingWithPromises_PromiseResolves) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://block.example/*", |
| "http://allow.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that cancels any requests that include |
| // block.example. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| let result = {}; |
| if (details.url.includes('block.example')) { |
| result.cancel = true; |
| } |
| return Promise.resolve(result); |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| // Navigate to allow.example. This should succeed. |
| { |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("allow.example", "/simple.html"))); |
| EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); |
| } |
| |
| // Now, navigate to block.example. This navigation should be blocked. |
| { |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_FALSE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("block.example", "/simple.html"))); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code()); |
| } |
| } |
| |
| // Tests an extension returning a promise that rejects from a webRequest |
| // blocking handler. The request should proceed. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| WebRequestBlockingWithPromises_PromiseRejects) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://test.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that cancels any requests that include |
| // block.example. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| return Promise.reject(new Error('Some Error')); |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| ErrorConsoleTestObserver error_observer(1u, profile()); |
| error_observer.EnableErrorCollection(); |
| ErrorConsole::Get(profile())->SetReportingAllForExtension(extension->id(), |
| true); |
| |
| // Navigate to test.example. The extension intercepts the request and returns |
| // a promise that rejects. |
| // This results in: |
| // - An error being logged in the extension context, and |
| // - The request proceeding. |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_TRUE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| "test.example", "/simple.html"))); |
| EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); |
| |
| error_observer.WaitForErrors(); |
| |
| const ErrorList& errors = |
| ErrorConsole::Get(profile())->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(1u, errors.size()); |
| EXPECT_TRUE(base::StartsWith(errors[0]->message(), |
| u"Uncaught (in promise) Error: Some Error")) |
| << errors[0]->message(); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests an extension returning a promise that never resolves from a webRequest |
| // blocking handler. The request should hang forever. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| WebRequestBlockingWithPromises_PromiseHangs) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://test.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that cancels any requests that include |
| // block.example. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| setTimeout(() => { |
| chrome.test.sendMessage('received event'); |
| }, 500); |
| return new Promise(() => { }); |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| |
| // Navigate to test.example. The extension intercepts this request, and then |
| // returns a promise that never resolves. This will result in the request |
| // hanging forever and ever, with no way for the user to do anything about |
| // it, possibly bricking the entire browser. (This is desired functionality |
| // for enterprise-installed extensions.) |
| // There's no good way to validate "will never resolve", but we do our best |
| // by waiting for an async message from the extension and validating the |
| // request is still in-flight. |
| ExtensionTestMessageListener listener("received event"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), embedded_test_server()->GetURL("test.example", "/simple.html"), |
| WindowOpenDisposition::CURRENT_TAB, ui_test_utils::BROWSER_TEST_NO_WAIT); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| |
| EXPECT_TRUE(web_contents->IsLoading()); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests a service worker-based extension registering multiple webRequest events |
| // in multiple contexts. This ensures the subevent name logic for service worker |
| // extensions doesn't result in any collisions of listener IDs, similar to the |
| // issue found in https://crbug.com/1297276. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| MultipleListenersAndContexts) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "storage"], |
| "host_permissions": [ |
| "http://first.example/*", |
| "http://second.example/*", |
| "http://third.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension has two contexts: the background service worker (which |
| // registers two listeners) and a separate page (which also registers a |
| // listener). This ensures that a) service worker listeners do not conflict |
| // with each other and b) service worker listeners do not conflict with |
| // listeners registered in other contexts. |
| static constexpr char kBackgroundJs[] = |
| R"(self.firstCount = 0; |
| self.secondCount = 0; |
| function firstListener() { ++firstCount; } |
| function secondListener() { ++secondCount; } |
| chrome.webRequest.onBeforeRequest.addListener( |
| firstListener, |
| {urls: ['http://first.example/*'], types: ['main_frame']}, []); |
| chrome.webRequest.onBeforeRequest.addListener( |
| secondListener, |
| {urls: ['http://second.example/*'], types: ['main_frame']}, []);)"; |
| static constexpr char kPageHtml[] = |
| R"(<!doctype html> |
| <html> |
| Page |
| <script src="page.js"></script> |
| </html>)"; |
| static constexpr char kPageJs[] = |
| R"(self.thirdCount = 0; |
| function thirdListener() { ++thirdCount; } |
| chrome.webRequest.onBeforeRequest.addListener( |
| thirdListener, |
| {urls: ['http://third.example/*'], types: ['main_frame']}, []);)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Load the page with the extension listeners. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, extension->GetResourceURL("page.html"))); |
| content::RenderFrameHost* page_host = web_contents->GetPrimaryMainFrame(); |
| ASSERT_TRUE(page_host); |
| |
| // At this point, 3 listeners should be registered. |
| EXPECT_EQ(3u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Convenience lambdas for checking the count received in each listener. |
| auto get_first_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), "firstCount"); |
| }; |
| auto get_second_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), "secondCount"); |
| }; |
| auto get_third_count = [page_host]() { |
| return content::EvalJs(page_host, "window.thirdCount;").ExtractInt(); |
| }; |
| |
| // No listeners should have fired yet. |
| EXPECT_EQ(0, get_first_count()); |
| EXPECT_EQ(0, get_second_count()); |
| EXPECT_EQ(0, get_third_count()); |
| |
| // Navigate to first.example (this first navigation needs to happen in a new |
| // tab so that we don't navigate the extension page). |
| NavigateToURLInNewTab( |
| embedded_test_server()->GetURL("first.example", "/title1.html")); |
| // Get the new web contents. |
| web_contents = GetActiveWebContents(); |
| |
| // (Only) the first listener should have fired. |
| EXPECT_EQ(1, get_first_count()); |
| EXPECT_EQ(0, get_second_count()); |
| EXPECT_EQ(0, get_third_count()); |
| |
| // Navigate to second.example. The second listener should fire. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("second.example", "/title1.html"))); |
| EXPECT_EQ(1, get_first_count()); |
| EXPECT_EQ(1, get_second_count()); |
| EXPECT_EQ(0, get_third_count()); |
| |
| // Navigate to third.example. The third listener should fire. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("third.example", "/title1.html"))); |
| EXPECT_EQ(1, get_first_count()); |
| EXPECT_EQ(1, get_second_count()); |
| EXPECT_EQ(1, get_third_count()); |
| } |
| |
| // Tests that a service worker-based extension with webRequestBlocking can |
| // intercept requests after the service worker stops. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| WebRequestBlocking_AfterWorkerShutdown) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://block.example/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that cancels any requests that include |
| // block.example. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| if (details.url.includes('block.example')) { |
| return {cancel: true} |
| } |
| return {}; |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| // A single webRequest listener should be registered. |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Stop the service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| // The listener should still be registered, but should be counted as an |
| // inactive listener. |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Navigate to block.example. The request should be blocked by the extension. |
| { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_FALSE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("block.example", "/simple.html"))); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code()); |
| } |
| } |
| |
| // Tests a service worker-based extension using webRequest for observational |
| // purposes receives events after the worker stops. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| WebRequestObservation_AfterWorkerShutdown) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "storage"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension that stores the number of matched requests in a count in |
| // extension storage. |
| // This is very similar to the test extension at |
| // chrome/test/data/extensions/api_test/webrequest_persistent, but is |
| // manifest V3. There's enough changes that our loading auto-conversion code |
| // won't quite work (mostly around permissions vs host_permissions), so we |
| // need a bit of a duplication here. |
| static constexpr char kBackgroundJs[] = |
| R"(let storageComplete = undefined; |
| let isUsingStorage = false; |
| |
| // Waits for any pending load to complete to avoid raciness in the |
| // test. |
| async function flushStorage() { |
| console.assert(!storageComplete); |
| if (!isUsingStorage) |
| return; |
| await new Promise((resolve) => { |
| storageComplete = resolve; |
| }); |
| storageComplete = undefined; |
| } |
| |
| chrome.webRequest.onBeforeRequest.addListener( |
| async (details) => { |
| isUsingStorage = true; |
| let {requestCount} = |
| await chrome.storage.local.get({requestCount: 0}); |
| requestCount++; |
| await chrome.storage.local.set({requestCount}); |
| isUsingStorage = false; |
| if (storageComplete) |
| storageComplete(); |
| chrome.test.sendMessage('event received'); |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']});)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadExtension( |
| test_dir.UnpackedPath(), {.wait_for_registration_stored = true}); |
| ASSERT_TRUE(extension); |
| |
| // A single listener should be registered. |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Navigate to a URL. The request should be seen by the extension. |
| auto* web_contents = GetActiveWebContents(); |
| EXPECT_TRUE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| "example.com", "/simple.html"))); |
| |
| auto get_request_count = [this, extension]() { |
| base::Value request_count = BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), kGetNumRequests, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| return request_count.GetInt(); |
| }; |
| |
| EXPECT_EQ(1, get_request_count()); |
| |
| // Stop the extension's service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| // The listener should still be registered, but should be counted as an |
| // inactive listener. |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| { |
| // Navigate again. The request should again be seen by the extension. |
| // |
| // We need to use a message listener here to ensure we gave the extension |
| // enough time to start up and have the event fire. Unlike the blocking |
| // scenario, there's no guarantee this happens by the time navigation |
| // completes. |
| ExtensionTestMessageListener listener("event received"); |
| EXPECT_TRUE(NavigateToURL( |
| web_contents, |
| embedded_test_server()->GetURL("example.com", "/simple.html"))); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // The inactive listener should have been reactivated... |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // ... and the extension should have seen the request. |
| EXPECT_EQ(2, get_request_count()); |
| } |
| |
| // Verifies that a failed dispatch to an inactive, non-blocking listener does |
| // not cause a request to bypass a blocking listener. Regression test for |
| // crbug.com/412695438. |
| IN_PROC_BROWSER_TEST_F( |
| ManifestV3WebRequestApiTest, |
| NonBlockingListenerFailureDoesNotBypassBlockingListener) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // An extension with a listener that cancels any requests that include |
| // example.com. |
| static constexpr char kBlockingManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kBlockingBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| if (details.url.includes('example.com')) { |
| return {cancel: true} |
| } |
| return {}; |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir blocking_test_dir; |
| blocking_test_dir.WriteManifest(kBlockingManifest); |
| blocking_test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kBlockingBackgroundJs); |
| const Extension* blocking_extension = LoadPolicyExtension(blocking_test_dir); |
| ASSERT_TRUE(blocking_extension); |
| |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // An extension with a non-blocking listener that registers the first |
| // time it's activated, but fails to do it the subsequent times. |
| static constexpr char kNonBlockingManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "storage"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kNonBlockingBackgroundJs[] = |
| R"(const key = 'listenerRegistered'; |
| chrome.storage.local.get([key], async (result) => { |
| if (!result[key]) { |
| chrome.webRequest.onBeforeRequest.addListener( |
| async (details) => {}, |
| {urls: ['<all_urls>'], types: ['main_frame']}); |
| await chrome.storage.local.set({[key]: true}); |
| } |
| chrome.test.sendMessage('ready'); |
| });)"; |
| |
| TestExtensionDir non_blocking_test_dir; |
| non_blocking_test_dir.WriteManifest(kNonBlockingManifest); |
| non_blocking_test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kNonBlockingBackgroundJs); |
| ExtensionTestMessageListener ready_listener("ready"); |
| const Extension* non_blocking_extension = |
| LoadExtension(non_blocking_test_dir.UnpackedPath()); |
| ASSERT_TRUE(non_blocking_extension); |
| ASSERT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| EXPECT_EQ(2u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Stop the non-blocking extension's service worker, making its listener |
| // inactive. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope( |
| profile(), non_blocking_extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| // There should now be one active listener (blocking) and one inactive |
| // listener (non-blocking). |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Stop the blocking extension's service worker too. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope( |
| profile(), blocking_extension->id()); |
| |
| // Navigation to example.com should be correctly blocked. |
| auto* web_contents = GetActiveWebContents(); |
| EXPECT_FALSE(NavigateToURL(web_contents, embedded_test_server()->GetURL( |
| "example.com", "/simple.html"))); |
| } |
| |
| // Tests unloading an extension with lazy listeners while the worker is |
| // inactive. The listeners should be properly cleaned up. |
| IN_PROC_BROWSER_TEST_F( |
| ManifestV3WebRequestApiTest, |
| ServiceWorkerWithWebRequest_UnloadExtensionWhileWorkerInactive) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| async (details) => { }, |
| {urls: ['<all_urls>'], types: ['main_frame']});)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadExtension( |
| test_dir.UnpackedPath(), {.wait_for_registration_stored = true}); |
| ASSERT_TRUE(extension); |
| |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| DisableExtension(extension->id()); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| } |
| |
| // Tests a service worker adding and then removing a listener. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| ServiceWorkerWithWebRequest_ManuallyRemoveListener) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kBackgroundJs[] = |
| R"(self.firstListenerCount = 0; |
| self.secondListenerCount = 0; |
| self.firstListener = function() { ++firstListenerCount; }; |
| self.secondListener = function() { ++secondListenerCount; }; |
| chrome.webRequest.onBeforeRequest.addListener( |
| firstListener, |
| {urls: ['<all_urls>'], types: ['main_frame']}); |
| chrome.webRequest.onBeforeRequest.addListener( |
| secondListener, |
| {urls: ['<all_urls>'], types: ['main_frame']});)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| |
| const Extension* extension = LoadExtension( |
| test_dir.UnpackedPath(), {.wait_for_registration_stored = true}); |
| ASSERT_TRUE(extension); |
| |
| // There should initially be two listeners registered, both active (since |
| // the service worker is active). |
| EXPECT_EQ(2u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Manually remove one of the listeners. This should result in the listener |
| // being fully removed (not deactivated), so there should only be a single |
| // listener remaining. |
| static constexpr char kRemoveListener[] = |
| R"(chrome.webRequest.onBeforeRequest.removeListener(self.firstListener); |
| chrome.test.sendScriptResult('');)"; |
| BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), kRemoveListener, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Navigate to a page and verify that only the second listener fires. |
| EXPECT_TRUE(NavigateToURL( |
| GetActiveWebContents(), |
| embedded_test_server()->GetURL("example.com", "/simple.html"))); |
| |
| EXPECT_EQ(0, GetCountFromBackgroundScript(extension, profile(), |
| "firstListenerCount")); |
| EXPECT_EQ(1, GetCountFromBackgroundScript(extension, profile(), |
| "secondListenerCount")); |
| } |
| |
| // Tests listeners in multiple contexts with lazy event disptaching. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| ListenersInMultipleContextsWithLazyDispatch) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ "http://example.com/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension has two contexts: the background service worker and a |
| // separate page, each of which register an identical listener. Each should |
| // only be invoked once. |
| static constexpr char kBackgroundJs[] = |
| R"(self.eventCount = 0; |
| chrome.webRequest.onBeforeRequest.addListener( |
| async function() { |
| ++eventCount; |
| // Perform a rount trip to ensure any events that are coming our |
| // way get dispatched, and then notify the test. |
| await chrome.test.waitForRoundTrip('test'); |
| chrome.test.sendMessage('worker received'); |
| }, |
| {urls: ['http://example.com/*'], types: ['main_frame']}, []);)"; |
| static constexpr char kPageHtml[] = |
| R"(<!doctype html> |
| <html> |
| Page |
| <script src="page.js"></script> |
| </html>)"; |
| static constexpr char kPageJs[] = |
| R"(self.eventCount = 0; |
| chrome.webRequest.onBeforeRequest.addListener( |
| function() { ++eventCount; }, |
| {urls: ['http://example.com/*'], types: ['main_frame']}, []);)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs); |
| const Extension* extension = LoadExtension( |
| test_dir.UnpackedPath(), {.wait_for_registration_stored = true}); |
| |
| ASSERT_TRUE(extension); |
| |
| // Load the page with the extension listeners. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, extension->GetResourceURL("page.html"))); |
| content::RenderFrameHost* page_host = web_contents->GetPrimaryMainFrame(); |
| ASSERT_TRUE(page_host); |
| |
| // At this point, 2 listeners should be registered. |
| EXPECT_EQ(2u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Convenience lambdas for checking the count received in each listener. |
| auto get_worker_event_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), "eventCount"); |
| }; |
| auto get_page_event_count = [page_host]() { |
| return content::EvalJs(page_host, "self.eventCount;").ExtractInt(); |
| }; |
| |
| // Stop the extension's service worker. The worker listener should now be |
| // registered as an inactive listener. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| { |
| ExtensionTestMessageListener listener("worker received"); |
| // Navigate to example.com (this navigation needs to happen in a new tab so |
| // that we don't navigate the extension page). |
| NavigateToURLInNewTab( |
| embedded_test_server()->GetURL("example.com", "/title1.html")); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Each listener should have fired exactly once. |
| EXPECT_EQ(1, get_worker_event_count()); |
| EXPECT_EQ(1, get_page_event_count()); |
| } |
| |
| // Tests listeners in the extension (extension tab) and extension background |
| // (service worker) contexts with lazy event dispatching. However, this |
| // simulates the worker never stopping and notifying that the worker's active |
| // listener should be removed. Regression test for crbug.com/331358156. |
| IN_PROC_BROWSER_TEST_F( |
| ManifestV3WebRequestApiTest, |
| ListenersInMultipleContextsWithLazyDispatch_ButActiveListenerRemovalStalled) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ "http://example.com/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension has two contexts: the background service worker and a |
| // separate page, each of which register an identical listener. Each should |
| // only be invoked once. |
| static constexpr char kBackgroundJs[] = |
| R"(self.eventCount = 0; |
| chrome.webRequest.onBeforeRequest.addListener( |
| async function() { |
| ++eventCount; |
| // Perform a round trip to ensure any events that are coming our |
| // way get dispatched, and then notify the test. |
| await chrome.test.waitForRoundTrip('test'); |
| chrome.test.sendMessage('worker received'); |
| }, |
| {urls: ['http://example.com/*'], types: ['main_frame']}, []);)"; |
| static constexpr char kPageHtml[] = |
| R"(<!doctype html> |
| <html> |
| Page |
| <script src="page.js"></script> |
| </html>)"; |
| static constexpr char kPageJs[] = |
| R"(self.eventCount = 0; |
| chrome.webRequest.onBeforeRequest.addListener( |
| function() { ++eventCount; }, |
| {urls: ['http://example.com/*'], types: ['main_frame']}, []);)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs); |
| const Extension* extension = LoadExtension( |
| test_dir.UnpackedPath(), {.wait_for_registration_stored = true}); |
| |
| ASSERT_TRUE(extension); |
| |
| // Load the page with the extension listeners. |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE( |
| NavigateToURL(web_contents, extension->GetResourceURL("page.html"))); |
| content::RenderFrameHost* page_host = web_contents->GetPrimaryMainFrame(); |
| ASSERT_TRUE(page_host); |
| |
| // At this point, 2 listeners should be registered. |
| EXPECT_EQ(2u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Convenience lambdas for checking the count received in each listener. |
| auto get_worker_event_count = [this, extension]() { |
| return GetCountFromBackgroundScript(extension, profile(), "eventCount"); |
| }; |
| auto get_page_event_count = [page_host]() { |
| return content::EvalJs(page_host, "self.eventCount;").ExtractInt(); |
| }; |
| |
| // Get the soon to be stopped ("previous") worker's `WorkerId`. |
| std::optional<WorkerId> previous_service_worker_id = |
| GetWorkerIdForExtension(extension->id()); |
| ASSERT_TRUE(previous_service_worker_id); |
| |
| // Setup intercept of `EventRouter::RemoveListenerForServiceWorker()` mojom |
| // call. This simulates the worker renderer thread being very slow/never |
| // informing the //extensions browser layer that the worker stopped and that |
| // it's active listeners should be removed. |
| EventRouterInterceptorForStopListenerRemoval |
| event_listener_removal_on_stop_interceptor( |
| profile(), previous_service_worker_id->render_process_id); |
| |
| // Stop the extension's service worker. The worker listener, due to the |
| // interceptor, will stay registered as an active listener. However, |
| // the worker task queue will catch when the worker begins stopping and remove |
| // the active listener. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| { |
| ExtensionTestMessageListener listener("worker received"); |
| // Navigate to example.com (this navigation needs to happen in a new tab so |
| // that we don't navigate the extension page). |
| NavigateToURLInNewTab( |
| embedded_test_server()->GetURL("example.com", "/title1.html")); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Each listener should have fired exactly once. |
| EXPECT_EQ(1, get_worker_event_count()); |
| EXPECT_EQ(1, get_page_event_count()); |
| |
| // Ensure the service worker that responded is a newly started instance. |
| std::optional<WorkerId> new_instance_service_worker_id = |
| GetWorkerIdForExtension(extension->id()); |
| ASSERT_TRUE(new_instance_service_worker_id); |
| EXPECT_NE(*previous_service_worker_id, *new_instance_service_worker_id); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests that an MV3 extension can use the `webRequestAuthProvider` permission |
| // to intercept and handle `onAuthRequired` events coming from a tab. |
| // TODO(crbug.com/371324825): Port to desktop Android. The navigation to the |
| // auth URL fails. Perhaps the webRequestAuthProvider permission isn't working, |
| // or Android handles http auth differently than desktop platforms. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, TestOnAuthRequiredTab) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestAuthProvider"], |
| "host_permissions": [ "http://example.com/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension will asynchronously provide the user credentials for the |
| // request. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onAuthRequired.addListener( |
| (details, callback) => { |
| chrome.test.assertEq('mv3authprovider', details.realm); |
| chrome.test.assertEq(401, details.statusCode); |
| const authCredentials = {username: 'foo', password: 'secret'}; |
| setTimeout(() => { |
| callback({authCredentials}); |
| chrome.test.succeed(); |
| }, 20); |
| }, |
| {urls: ['<all_urls>']}, |
| ['asyncBlocking']);)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a special URL that will prompt the user for credentials. The |
| // request should succeed (verified by the last navigation status) and the |
| // extension should have received the event (verified by the ResultCatcher). |
| static constexpr char kRealm[] = "mv3authprovider"; |
| std::string auth_url_path = |
| base::StringPrintf("/auth-basic/%s/subpath?realm=%s", kRealm, kRealm); |
| GURL auth_url = embedded_test_server()->GetURL("example.com", auth_url_path); |
| |
| ResultCatcher result_catcher; |
| auto* web_contents = GetActiveWebContents(); |
| content::TestNavigationObserver navigation_observer(web_contents); |
| ASSERT_TRUE(NavigateToURL(web_contents, auth_url)); |
| ASSERT_TRUE(result_catcher.GetNextResult()); |
| EXPECT_EQ(auth_url, web_contents->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| class ManifestV3WebRequestApiTestWithBypassRedirectChecksPerRequest |
| : public ManifestV3WebRequestApiTest, |
| public base::test::WithFeatureOverride { |
| public: |
| ManifestV3WebRequestApiTestWithBypassRedirectChecksPerRequest() |
| : WithFeatureOverride(features::kBypassRedirectChecksPerRequest) {} |
| }; |
| |
| // Tests service workers can't redirect to unsafe URLs when WebRequest |
| // extensions are proxying requests. Regression test for crbug.com/379337758. |
| IN_PROC_BROWSER_TEST_P( |
| ManifestV3WebRequestApiTestWithBypassRedirectChecksPerRequest, |
| ServiceWorkerCantRedirectToUnsafeUrls) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": ["<all_urls>"], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // Listen to all requests to ensure they're proxied. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { return {}; }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| // Setup a service worker that redirects all requests to a "data:" URL. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| const GURL sw_url = embedded_test_server()->GetURL( |
| "/service_worker/service_worker_setup_data_redirect.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, sw_url)); |
| EXPECT_EQ("ok", EvalJs(web_contents, "setup();")); |
| |
| // Navigate to a URL that the service worker is monitoring. |
| content::TestNavigationObserver nav_observer(web_contents); |
| const GURL url = embedded_test_server()->GetURL("/service_worker/test"); |
| |
| const bool per_request_bypass = IsParamFeatureEnabled(); |
| if (per_request_bypass) { |
| EXPECT_FALSE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, nav_observer.last_net_error_code()); |
| } else { |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); |
| } |
| } |
| |
| // Toggle `features::BypassRedirectChecksPerRequest`. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE( |
| ManifestV3WebRequestApiTestWithBypassRedirectChecksPerRequest); |
| |
| class OnAuthRequiredApiTest : public ExtensionApiTest { |
| public: |
| static constexpr char kTestDomain[] = "a.test"; |
| OnAuthRequiredApiTest() { |
| // Https is required to use service workers. |
| // This limits the set of domains with valid certificates. For the purposes |
| // of this test we will use kTestDomain. |
| UseHttpsTestServer(); |
| } |
| ~OnAuthRequiredApiTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| embedded_test_server()->ServeFilesFromSourceDirectory("chrome/test/data"); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| } |
| |
| // Returns a URL which requires username/password |
| GURL MakeAuthUrl() { |
| static constexpr char kRealm[] = "mv3authprovider"; |
| std::string auth_url_path = |
| base::StringPrintf("/auth-basic/%s/subpath?realm=%s", kRealm, kRealm); |
| return embedded_test_server()->GetURL(kTestDomain, auth_url_path); |
| } |
| |
| // Loads an extension that implements onAuthRequired. `additional_js` will be |
| // concatenated to the background.js. |
| void LoadExtensionWithAdditionalJs(const std::string& additional_js) { |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestAuthProvider"], |
| "host_permissions": [ "http://127.0.0.1/*", "https://a.test/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kBackgroundJs[] = |
| R"( |
| let didInterceptAuth = false; |
| chrome.webRequest.onAuthRequired.addListener( |
| (details, callback) => { |
| didInterceptAuth = true; |
| chrome.test.assertEq('mv3authprovider', details.realm); |
| chrome.test.assertEq(401, details.statusCode); |
| const authCredentials = {username: 'foo', password: 'secret'}; |
| callback({authCredentials}); |
| }, |
| {urls: ['<all_urls>']}, |
| ['asyncBlocking']); |
| )"; |
| std::string background_js(kBackgroundJs); |
| background_js += additional_js; |
| |
| test_extension_dir_.WriteManifest(kManifest); |
| test_extension_dir_.WriteFile(FILE_PATH_LITERAL("background.js"), |
| background_js); |
| |
| const Extension* extension = |
| LoadExtension(test_extension_dir_.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| } |
| |
| private: |
| TestExtensionDir test_extension_dir_; |
| base::ScopedTempDir service_worker_dir_; |
| }; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Tests that an MV3 extension can use the `webRequestAuthProvider` permission |
| // to intercept and handle `onAuthRequired` events coming from an extension |
| // service worker. This test does the following: |
| // (1) This loads an extension with a service-worker background.js. |
| // (2) The extension adds a listener to chrome.webRequest.onAuthRequired. |
| // (3) The extension attempts to fetch a resource that requires http auth. |
| // (4) This triggers the listener in (3), which supplies credentials |
| // (5) Checks that the fetch succeeded. |
| // Fails on Android crbug.com/371324825 |
| IN_PROC_BROWSER_TEST_F(OnAuthRequiredApiTest, |
| TestOnAuthRequiredExtensionServiceWorker) { |
| // After the extension loads, trigger an async request to fetch an http auth |
| // resource. |
| std::string additional_js = |
| R"( |
| (async function() { |
| try { |
| const response = await fetch($1); |
| if (response.ok) { |
| chrome.test.assertTrue(didInterceptAuth); |
| chrome.test.succeed(); |
| } else { |
| chrome.test.fail(); |
| } |
| } catch (e) { |
| chrome.test.fail(); |
| } |
| })(); |
| )"; |
| additional_js = content::JsReplace(additional_js, MakeAuthUrl()); |
| |
| // Loading the extension triggers the remaining steps of the test. |
| ResultCatcher result_catcher; |
| LoadExtensionWithAdditionalJs(additional_js); |
| |
| ASSERT_TRUE(result_catcher.GetNextResult()); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // This test is similar to TestOnAuthRequiredExtensionServiceWorker but the |
| // service worker is hosted by a website instead of the extension istelf. |
| IN_PROC_BROWSER_TEST_F(OnAuthRequiredApiTest, |
| TestOnAuthRequiredWebsiteServiceWorker) { |
| // Load the extension. |
| LoadExtensionWithAdditionalJs(""); |
| |
| // Navigate to the test page. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| GURL requestor_url = embedded_test_server()->GetURL( |
| kTestDomain, "/ssl/service_worker_fetch/page.html"); |
| ASSERT_TRUE(NavigateToURL(web_contents, requestor_url)); |
| |
| // Perform a fetch from a worker and validate that it succeeds. |
| std::string fetch_response = |
| content::EvalJs(web_contents, |
| content::JsReplace("doFetchInWorker($1);", MakeAuthUrl())) |
| .ExtractString(); |
| EXPECT_THAT(fetch_response, testing::HasSubstr("<title>")); |
| } |
| |
| // Tests the behavior of an extension that registers an event listener |
| // asynchronously. |
| // Regression test for https://crbug.com/1397879 and https://crbug.com/1434212. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, AsyncListenerRegistration) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // A background context that *conditionally* registers a blocking listener. |
| // We send a "will_register" message and register the listener once we receive |
| // the response from that message. If we never receive a response, we never |
| // register the event listener. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.test.sendMessage('will_register').then(() => { |
| chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| if (details.url.includes('example.com')) { |
| return {cancel: true} |
| } |
| return {}; |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('registered'); |
| }); |
| // Register an additional event properly so that the service worker |
| // still has _a_ listener registered in the process. |
| // https://crbug.com/1434212. |
| chrome.webRequest.onHeadersReceived.addListener( |
| (details) => {}, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| // Load the extension and tell it to register the listener. |
| ExtensionTestMessageListener will_register_listener( |
| "will_register", ReplyBehavior::kWillReply); |
| ExtensionTestMessageListener registered_listener("registered"); |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(will_register_listener.WaitUntilSatisfied()); |
| EXPECT_FALSE(registered_listener.was_satisfied()); |
| will_register_listener.Reply("Go for it!"); |
| ASSERT_TRUE(registered_listener.WaitUntilSatisfied()); |
| |
| // A single webRequest listener should be registered. |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(0u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| const GURL url = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| |
| // Navigate to example.com to check our setup; the request should be blocked. |
| { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_FALSE(NavigateToURL(web_contents, url)); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code()); |
| } |
| |
| // Stop the service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| // Note: the task to remove listeners from ExtensionWebRequestEventRouter |
| // is async; run to flush the posted task. |
| base::RunLoop().RunUntilIdle(); |
| |
| // The listener should still be registered, but should be counted as an |
| // inactive listener. |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| EXPECT_EQ(1u, web_request_router()->GetInactiveListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Reset the "will register" listener. However, we'll never reply this time, |
| // which means the extension will never register the listener again. |
| will_register_listener.Reset(); |
| |
| // Now, navigate to example.com again. This will wake up the extension service |
| // worker, but we'll fail to dispatch the event to the extension because the |
| // listener isn't registered. The request should be allowed to continue. |
| { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| content::TestNavigationObserver nav_observer(web_contents); |
| EXPECT_TRUE(NavigateToURL(web_contents, url)); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(net::OK, nav_observer.last_net_error_code()); |
| } |
| |
| // Clean up: ExtensionTestMessageListener requires a reply (or else will |
| // DCHECK). Wait for it to receive the message (it probably already did, but |
| // theoretically can race), and send a response. |
| EXPECT_TRUE(will_register_listener.WaitUntilSatisfied()); |
| will_register_listener.Reply("unused"); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // uses ui_test_utils |
| // Tests behavior when a service worker is stopped while processing an event. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| ServiceWorkerGoesAwayWhileHandlingRequest) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest", "webRequestBlocking"], |
| "host_permissions": [ |
| "http://example.com/*" |
| ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // An extension with a listener that will spin forever on example.com |
| // requests. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onBeforeRequest.addListener( |
| (details) => { |
| if (details.url.includes('example.com')) { |
| chrome.test.sendMessage('received'); |
| // Spin FOREVER. |
| while (true) { } |
| } |
| return {}; |
| }, |
| {urls: ['<all_urls>'], types: ['main_frame']}, |
| ['blocking']); |
| chrome.test.sendMessage('ready');)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadPolicyExtension(test_dir); |
| ASSERT_TRUE(extension); |
| |
| // A single webRequest listener should be registered. |
| EXPECT_EQ(1u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| |
| // Navigate to example.com; the extension will receive the event and spin |
| // indefinitely. |
| // We navigate in a new tab to have a better signal of "request started". |
| // We can't wait for the request to finish, since the extension's listener |
| // never returns, which blocks the request. |
| const GURL url = |
| embedded_test_server()->GetURL("example.com", "/simple.html"); |
| content::TestNavigationObserver nav_observer(url); |
| nav_observer.StartWatchingNewWebContents(); |
| ExtensionTestMessageListener test_listener("received"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| EXPECT_TRUE(test_listener.WaitUntilSatisfied()); |
| // The web contents should still be loading, and should have no last |
| // committed URL since the extension is blocking the request. |
| EXPECT_TRUE(web_contents->IsLoading()); |
| EXPECT_EQ(GURL(), web_contents->GetLastCommittedURL()); |
| |
| // Stop the extension service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension->id()); |
| |
| // The request should be unblocked. |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents)); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(url, web_contents->GetLastCommittedURL()); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // Tests that a MV3 extension that doesn't have the `webRequestAuthProvider` |
| // permission cannot use blocking listeners for `onAuthRequired`. |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| TestOnAuthRequired_NoPermission) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ "http://example.com/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension tries to add a listener; this will fail asynchronously |
| // as a part of the webRequestInternal API trying to add the listener. |
| // This results in runtime.lastError being set, but since it's an |
| // internal API, there's no way for the extension to catch the error. |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.webRequest.onAuthRequired.addListener( |
| (details, callback) => {}, |
| {urls: ['<all_urls>']}, |
| ['asyncBlocking']);)"; |
| |
| // Since we can't catch the error in the extension's JS, we instead listen to |
| // the error come into the error console. |
| ErrorConsoleTestObserver error_observer(1u, profile()); |
| error_observer.EnableErrorCollection(); |
| |
| // Load the extension and wait for the error to come. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| |
| ASSERT_TRUE(extension); |
| error_observer.WaitForErrors(); |
| |
| const ErrorList& errors = |
| ErrorConsole::Get(profile())->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(1u, errors.size()); |
| EXPECT_TRUE( |
| base::StartsWith(errors[0]->message(), |
| u"Unchecked runtime.lastError: You do not have " |
| u"permission to use blocking webRequest listeners.")) |
| << errors[0]->message(); |
| } |
| |
| // Tests that an extension that doesn't have the `webView` permission cannot |
| // manually create and add a WebRequestEvent that specifies a webViewInstanceId. |
| // TODO(tjudkins): It would be good to also stop this on the JS layer by not |
| // allowing extensions to manually create and add WebRequestEvents. |
| // Regression test for crbug.com/1472830 |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, |
| TestWebviewIdSpecifiedOnEvent_NoPermission) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "permissions": ["webRequest"], |
| "host_permissions": [ "http://example.com/*" ], |
| "background": {"service_worker": "background.js"} |
| })"; |
| // The extension tries to add a listener; this will fail asynchronously |
| // as a part of the webRequestInternal API trying to add the listener. |
| // This results in runtime.lastError being set, but since it's an |
| // internal API, there's no way for the extension to catch the error. |
| static constexpr char kBackgroundJs[] = |
| R"(let event = new chrome.webRequest.onBeforeRequest.constructor( |
| 'webRequest.onBeforeRequest', |
| undefined, |
| undefined, |
| undefined, |
| 1); // webViewInstanceId |
| event.addListener(() => {}, |
| {urls: ['*://*.example.com/*']});)"; |
| |
| // Since we can't catch the error in the extension's JS, we instead listen to |
| // the error come into the error console. |
| ErrorConsoleTestObserver error_observer(1u, profile()); |
| error_observer.EnableErrorCollection(); |
| |
| // Load the extension and wait for the error to come. |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest(kManifest); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| |
| ASSERT_TRUE(extension); |
| error_observer.WaitForErrors(); |
| |
| const ErrorList& errors = |
| ErrorConsole::Get(profile())->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(1u, errors.size()); |
| EXPECT_EQ(u"Unchecked runtime.lastError: Missing webview permission.", |
| errors[0]->message()); |
| EXPECT_EQ(0u, web_request_router()->GetListenerCountForTesting( |
| profile(), "webRequest.onBeforeRequest")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ManifestV3WebRequestApiTest, RecordUkmOnNavigation) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| TestExtensionDir test_dir1; |
| test_dir1.WriteManifest(R"({ |
| "name": "MV3 WebRequest", |
| "version": "0.1", |
| "manifest_version": 3, |
| "content_scripts": [ |
| { |
| "matches": ["<all_urls>"], |
| "js": ["contentscript.js"] |
| } |
| ], |
| "permissions": [ |
| "webRequest", |
| "webRequestBlocking", |
| "webRequestAuthProvider", |
| "declarativeNetRequest", |
| "declarativeNetRequestFeedback", |
| "declarativeNetRequestWithHostAccess" |
| ], |
| "host_permissions": ["http://a.com/*"], |
| "background": {"service_worker": "background.js"} |
| })"); |
| test_dir1.WriteFile(FILE_PATH_LITERAL("contentscript.js"), /*contents=*/""); |
| test_dir1.WriteFile(FILE_PATH_LITERAL("background.js"), |
| "chrome.test.sendMessage('ready');"); |
| ASSERT_TRUE(LoadPolicyExtension(test_dir1)); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // declarativeWebRequest is only supported by manifest version 2 or lower. |
| // Android doesn't support manifest version 2. |
| TestExtensionDir test_dir2; |
| test_dir2.WriteManifest(R"({ |
| "name": "MV2 WebRequest", |
| "version": "0.1", |
| "manifest_version": 2, |
| "content_scripts": [ |
| { |
| "matches": ["<all_urls>"], |
| "js": ["contentscript.js"] |
| } |
| ], |
| "permissions": [ |
| "declarativeWebRequest", |
| "http://b.com/*" |
| ], |
| "background": {"scripts": ["background.js"], "persistent": true} |
| })"); |
| test_dir2.WriteFile(FILE_PATH_LITERAL("contentscript.js"), /*contents=*/""); |
| test_dir2.WriteFile(FILE_PATH_LITERAL("background.js"), |
| "chrome.test.sendMessage('ready');"); |
| ExtensionTestMessageListener listener("ready"); |
| ASSERT_TRUE(LoadExtension(test_dir2.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| // We don't support manifest version 2 on android, so only the first extension |
| // will be loaded on android. |
| #if BUILDFLAG(IS_ANDROID) |
| const size_t expected_entries = 1; |
| #else |
| const size_t expected_entries = 2; |
| #endif |
| |
| base::RunLoop ukm_loop; |
| ukm::TestAutoSetUkmRecorder ukm_recorder; |
| ukm_recorder.SetOnAddEntryCallback( |
| ukm::builders::Extensions_OnNavigation::kEntryName, |
| base::BindLambdaForTesting([&]() { |
| if (ukm_recorder |
| .GetMergedEntriesByName( |
| ukm::builders::Extensions_OnNavigation::kEntryName) |
| .size() == expected_entries) { |
| ukm_loop.Quit(); |
| } |
| })); |
| |
| auto* web_contents = GetActiveWebContents(); |
| const GURL kUrlA = embedded_test_server()->GetURL("a.com", "/simple.html"); |
| EXPECT_TRUE(NavigateToURL(web_contents, kUrlA)); |
| |
| const GURL kUrlB = embedded_test_server()->GetURL("b.com", "/simple.html"); |
| #if !BUILDFLAG(IS_ANDROID) |
| EXPECT_TRUE(NavigateToURL(web_contents, kUrlB)); |
| #endif |
| |
| // Waits until UKM data is recorded. |
| ukm_loop.Run(); |
| |
| const double kBucketSpacing = 2; |
| auto merged_entries = ukm_recorder.GetMergedEntriesByName( |
| ukm::builders::Extensions_OnNavigation::kEntryName); |
| EXPECT_EQ(expected_entries, merged_entries.size()); |
| for (const auto& entry : merged_entries) { |
| const ukm::mojom::UkmEntry* ukm_entry = entry.second.get(); |
| const GURL& url = |
| ukm_recorder.GetSourceForSourceId(ukm_entry->source_id)->url(); |
| ukm_recorder.ExpectEntrySourceHasUrl(ukm_entry, url); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "EnabledExtensionCount", |
| ukm::GetExponentialBucketMin(expected_entries, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "EnabledExtensionCount.InjectContentScript", |
| ukm::GetExponentialBucketMin(expected_entries, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "EnabledExtensionCount.HaveHostPermissions", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| if (url == kUrlA) { |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestAuthProviderPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestBlockingPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestFeedbackPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestWithHostAccessPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeWebRequestPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| } else if (url == kUrlB) { |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestAuthProviderPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestBlockingPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "WebRequestPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestFeedbackPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeNetRequestWithHostAccessPermissionCount", |
| ukm::GetExponentialBucketMin(0u, kBucketSpacing)); |
| ukm::TestAutoSetUkmRecorder::ExpectEntryMetric( |
| ukm_entry, "DeclarativeWebRequestPermissionCount", |
| ukm::GetExponentialBucketMin(1u, kBucketSpacing)); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| // Allows test to wait for the failure of a worker registration. |
| class WorkerRegistrationFailureObserver |
| : public ServiceWorkerTaskQueue::TestObserver { |
| public: |
| explicit WorkerRegistrationFailureObserver(const ExtensionId extension_id) |
| : extension_id_(extension_id) { |
| ServiceWorkerTaskQueue::SetObserverForTest(this); |
| } |
| ~WorkerRegistrationFailureObserver() override { |
| ServiceWorkerTaskQueue::SetObserverForTest(nullptr); |
| } |
| |
| blink::ServiceWorkerStatusCode WaitForWorkerRegistrationFailure() { |
| if (!status_code_) { |
| SCOPED_TRACE("Waiting for worker registration to fail"); |
| failure_loop_.Run(); |
| } |
| return *status_code_; |
| } |
| |
| private: |
| void OnWorkerRegistrationFailed( |
| const ExtensionId& extension_id, |
| blink::ServiceWorkerStatusCode status_code) override { |
| if (extension_id == extension_id_) { |
| status_code_ = status_code; |
| failure_loop_.Quit(); |
| } |
| } |
| |
| ExtensionId extension_id_; |
| base::RunLoop failure_loop_; |
| std::optional<blink::ServiceWorkerStatusCode> status_code_; |
| }; |
| |
| // Allows test to wait for the call of `ResetURLLoaderFactories()` in |
| // WebRequestAPI. |
| class URLLoaderFactoriesResetWaiter : public WebRequestAPI::TestObserver { |
| public: |
| URLLoaderFactoriesResetWaiter() { WebRequestAPI::SetObserverForTest(this); } |
| ~URLLoaderFactoriesResetWaiter() override { |
| WebRequestAPI::SetObserverForTest(nullptr); |
| } |
| |
| URLLoaderFactoriesResetWaiter(const URLLoaderFactoriesResetWaiter&) = delete; |
| URLLoaderFactoriesResetWaiter& operator=( |
| const URLLoaderFactoriesResetWaiter&) = delete; |
| |
| void WaitForResetURLLoaderFactoriesCalled() { |
| SCOPED_TRACE("Waiting for ResetURLLoaderFactories to be called"); |
| url_loader_factory_reset_runloop_.Run(); |
| } |
| |
| private: |
| void OnDidResetURLLoaderFactories() override { |
| url_loader_factory_reset_runloop_.Quit(); |
| } |
| |
| base::RunLoop url_loader_factory_reset_runloop_; |
| }; |
| |
| class ManifestV3WebRequestApiTestWithSkipResetServiceWorkerURLLoaderFactories |
| : public ManifestV3WebRequestApiTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| ManifestV3WebRequestApiTestWithSkipResetServiceWorkerURLLoaderFactories() { |
| feature_list_.InitWithFeatureState( |
| extensions_features::kSkipResetServiceWorkerURLLoaderFactories, |
| GetParam()); |
| } |
| ~ManifestV3WebRequestApiTestWithSkipResetServiceWorkerURLLoaderFactories() |
| override = default; |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that the call to `ResetURLLoaderFactories()` performed by WebRequestAPI |
| // doesn't break the registration process of other extensions. |
| // Regression test for https://crbug.com/394523691. |
| IN_PROC_BROWSER_TEST_P( |
| ManifestV3WebRequestApiTestWithSkipResetServiceWorkerURLLoaderFactories, |
| ResetURLLoaderFactoryDoesntBreakRegistration) { |
| // Skip if the proxy is forced since factories will not be reset in that case. |
| if (base::FeatureList::IsEnabled( |
| extensions_features::kForceWebRequestProxyForTest)) { |
| return; |
| } |
| |
| bool feature_enabled = GetParam(); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // A simple extension that sends a message and waits for a response in its |
| // background script. |
| const ExtensionId extension_id("iegclhlplifhodhkoafiokenjoapiobj"); |
| static constexpr const char kKey[] = |
| "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P" |
| "Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf" |
| "qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv" |
| "7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i" |
| "L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz" |
| "afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB"; |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "TestExtension", |
| "manifest_version": 3, |
| "version": "0.1", |
| "key": "%s", |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kBackgroundJs[] = |
| R"(chrome.test.sendMessage('will_receive').then(() => { |
| console.log('received'); |
| }))"; |
| TestExtensionDir extension_dir; |
| extension_dir.WriteManifest(base::StringPrintf(kManifest, kKey)); |
| extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| ASSERT_TRUE(task_queue); |
| WebRequestAPI* web_request_api = |
| BrowserContextKeyedAPIFactory<WebRequestAPI>::Get(profile()); |
| ASSERT_TRUE(web_request_api); |
| |
| // Listen to "will_receive" message from the extension. |
| ExtensionTestMessageListener will_receive_listener( |
| "will_receive", |
| feature_enabled ? ReplyBehavior::kWillReply : ReplyBehavior::kWontReply); |
| // Listen to the completion of the registration storage. |
| service_worker_test_utils::TestServiceWorkerContextObserver |
| registration_observer(profile()); |
| // Listen for a failure in the worker registration. |
| WorkerRegistrationFailureObserver worker_failure_observer(extension_id); |
| |
| // Asynchronously load the extension so we can wait for a step in the loading |
| // process in the test. |
| std::optional<base::UnguessableToken> activation_token; |
| ChromeTestExtensionLoader(profile()).LoadUnpackedExtensionAsync( |
| extension_dir.UnpackedPath(), |
| base::BindLambdaForTesting([&](const Extension* extension) { |
| ASSERT_TRUE(extension); |
| activation_token = |
| task_queue->GetCurrentActivationToken(extension->id()); |
| ASSERT_TRUE(activation_token.has_value()); |
| })); |
| // ...and wait for the moment right after the worker is requested to start |
| // during the registration process. |
| registration_observer.WaitForStartWorkerMessageSent(); |
| URLLoaderFactoriesResetWaiter url_loader_factories_reset_waiter; |
| // Simulate the effect of loading an extension with WebRequestAPI permissions. |
| // In other words, make sure we're proxying for the current profile. |
| // This will cause WebRequestAPI to attempt calling |
| // `ResetURLLoaderFactories()`. Because the extension is still in the early |
| // phases of starting its worker here, this would break its registration |
| // before it has a chance of being completed and stored. |
| // Instead, `ResetURLLoaderFactories()` will skip resetting extension service |
| // worker URLLoaderFactories used for fetching scripts and sub-resources. |
| // NOTE: We simulate the call to `ResetURLLoaderFactories()` rather than |
| // loading an extension with WebRequestAPI permissions, as that would take too |
| // long and won't trigger the bug in all cases. |
| web_request_api->ForceProxyForTesting(); |
| |
| if (feature_enabled) { |
| // SkipResetServiceWorkerURLLoaderFactories feature enabled: expect |
| // successful execution. Check that the worker is still running and |
| // functional. |
| registration_observer.WaitForWorkerStarted(); |
| std::optional<WorkerId> worker_id = GetWorkerIdForExtension(extension_id); |
| EXPECT_TRUE(worker_id); |
| SCOPED_TRACE( |
| "Waiting for extension background to signal that it can send messages"); |
| ASSERT_TRUE(will_receive_listener.WaitUntilSatisfied()); |
| will_receive_listener.Reply("go"); |
| url_loader_factories_reset_waiter.WaitForResetURLLoaderFactoriesCalled(); |
| registration_observer.WaitForRegistrationStored(); |
| } else { |
| // SkipResetServiceWorkerURLLoaderFactories feature disabled: expect worker |
| // registration to fail. We have observed that the registration can fail |
| // with either `kErrorStartWorkerFailed` or `kErrorNetwork` depending on |
| // when exactly it's interrupted. |
| auto status_code = |
| worker_failure_observer.WaitForWorkerRegistrationFailure(); |
| EXPECT_NE(status_code, blink::ServiceWorkerStatusCode::kOk); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| ManifestV3WebRequestApiTestWithSkipResetServiceWorkerURLLoaderFactories, |
| testing::Bool()); |
| |
| } // namespace extensions |