| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/json/json_reader.h" |
| #include "base/path_service.h" |
| #include "base/task/current_thread.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/controlled_frame/controlled_frame_test_base.h" |
| #include "chrome/browser/extensions/browsertest_util.h" |
| #include "chrome/browser/extensions/menu_manager.h" |
| #include "chrome/browser/extensions/service_worker_apitest.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/embedder_support/user_agent_utils.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_exposed_isolation_level.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_utils.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/event_router.h" |
| #include "extensions/browser/guest_view/web_view/web_view_guest.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/install_default_websocket_handlers.h" |
| #include "net/test/test_data_directory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h" |
| |
| using testing::HasSubstr; |
| using testing::Not; |
| |
| namespace controlled_frame { |
| |
| namespace { |
| |
| constexpr char kWebRequestOnBeforeRequestEventName[] = |
| "webViewInternal.onBeforeRequest"; |
| constexpr char kWebRequestOnAuthRequiredEventName[] = |
| "webViewInternal.onAuthRequired"; |
| constexpr char kEvalSuccessStr[] = "SUCCESS"; |
| constexpr char kExpectedPropertiesJsonPath[] = |
| "controlled_frame/resources/expected_properties.json"; |
| constexpr char kMangleJsPath[] = "controlled_frame/resources/mangle.js"; |
| |
| std::string ReadTestDataFile(const std::string& test_data_relative_path) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath test_data_dir; |
| CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir)); |
| base::FilePath expected_properties_json_path = |
| test_data_dir.AppendASCII(test_data_relative_path); |
| std::string file_contents; |
| CHECK(base::ReadFileToString(expected_properties_json_path, &file_contents)); |
| return file_contents; |
| } |
| |
| const content::EvalJsResult SetBackgroundColorToWhite( |
| extensions::WebViewGuest* guest) { |
| return content::EvalJs(guest->GetGuestMainFrame(), R"( |
| (function() { |
| document.body.style.backgroundColor = 'white'; |
| return 'SUCCESS'; |
| })(); |
| )"); |
| } |
| |
| const content::EvalJsResult ExecuteScriptRedBackgroundCode( |
| content::RenderFrameHost* app_frame) { |
| return content::EvalJs(app_frame, R"( |
| (async function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return 'FAIL'; |
| } |
| await frame.executeScript( |
| {code: "document.body.style.backgroundColor = 'red';"}); |
| return 'SUCCESS'; |
| })(); |
| )"); |
| } |
| |
| const content::EvalJsResult ExecuteScriptRedBackgroundFile( |
| content::RenderFrameHost* app_frame) { |
| return content::EvalJs(app_frame, R"( |
| (async function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return 'FAIL'; |
| } |
| await frame.executeScript({file: "/execute_script.input.js"}); |
| return 'SUCCESS'; |
| })(); |
| )"); |
| } |
| |
| const content::EvalJsResult VerifyBackgroundColorIsRed( |
| extensions::WebViewGuest* guest) { |
| return content::EvalJs(guest->GetGuestMainFrame(), R"( |
| (function() { |
| if (document.body.style.backgroundColor === 'red') { |
| return 'SUCCESS'; |
| } else { |
| return 'FAIL'; |
| } |
| })(); |
| )"); |
| } |
| |
| // TODO(odejesush): Add tests for the rest of the Promise API methods. |
| const char* kControlledFramePromiseApiMethods[]{"back", "forward", "go"}; |
| |
| } // namespace |
| |
| class ControlledFrameApiTest : public ControlledFrameTestBase { |
| protected: |
| ControlledFrameApiTest() |
| : ControlledFrameTestBase( |
| /*channel=*/version_info::Channel::STABLE, |
| /*feature_setting=*/FeatureSetting::ENABLED, |
| /*flag_setting=*/FlagSetting::CONTROLLED_FRAME) {} |
| |
| ControlledFrameApiTest(const version_info::Channel& channel, |
| const FeatureSetting& feature_setting, |
| const FlagSetting& flag_setting) |
| : ControlledFrameTestBase(channel, feature_setting, flag_setting) {} |
| |
| testing::AssertionResult SetUseMangledJs(content::RenderFrameHost* frame) { |
| std::string mangle_js = ReadTestDataFile(kMangleJsPath); |
| if (mangle_js.length() == 0u) { |
| return testing::AssertionFailure() << "No mangle.js code found"; |
| } |
| return ExecJs(frame, mangle_js); |
| } |
| |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameTestBase::SetUpOnMainThread(); |
| StartContentServer("web_apps/simple_isolated_app"); |
| } |
| |
| }; |
| |
| // This test checks if the Controlled Frame is able to intercept URL navigation |
| // requests. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, URLLoaderIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const GURL& kOriginalControlledFrameUrl = |
| embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, kOriginalControlledFrameUrl)); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| const std::string& kServerHostPort = |
| embedded_https_test_server().host_port_pair().ToString(); |
| EXPECT_EQ("SUCCESS", |
| content::EvalJs(app_frame, content::JsReplace(R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return 'FAIL: frame or frame.request is undefined'; |
| } |
| frame.request.createWebRequestInterceptor({ |
| urlPatterns: ['*://*/*'], |
| resourceTypes: ['main-frame'], |
| blocking: true, |
| }).addEventListener('beforerequest', (e) => { |
| if (e.request.url.endsWith('cancel.html')) { |
| e.preventDefault(); |
| } |
| if (e.request.url.endsWith('redirect.html')) { |
| e.redirect('https://' + $1 + '/controlled_frame_redirect_target.html'); |
| } |
| }); |
| return 'SUCCESS'; |
| })(); |
| )", |
| kServerHostPort))); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| |
| // Check that navigations can be cancelled. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, net::Error::ERR_BLOCKED_BY_CLIENT, |
| content::MessageLoopRunner::QuitMode::IMMEDIATE, |
| /*ignore_uncommitted_navigations=*/false); |
| web_view_guest->NavigateGuest(embedded_https_test_server() |
| .GetURL("/controlled_frame_cancel.html") |
| .spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(net::Error::ERR_BLOCKED_BY_CLIENT, |
| navigation_observer.last_net_error_code()); |
| EXPECT_EQ(kOriginalControlledFrameUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_FALSE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that navigations can be redirected. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(embedded_https_test_server() |
| .GetURL("/controlled_frame_redirect.html") |
| .spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(embedded_https_test_server().GetURL( |
| "/controlled_frame_redirect_target.html"), |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that navigations can succeed. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, /*expected_number_of_navigations=*/1u); |
| const GURL& kControlledFrameSuccessUrl = |
| embedded_https_test_server().GetURL("/controlled_frame_success.html"); |
| web_view_guest->NavigateGuest(kControlledFrameSuccessUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kControlledFrameSuccessUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, AuthRequestIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnAuthRequiredEventName)); |
| |
| EXPECT_EQ(true, content::EvalJs(app_frame, R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| |
| const expectedUsername = 'test'; |
| const expectedPassword = 'pass'; |
| frame.request.createWebRequestInterceptor({ |
| urlPatterns: [`https://*/auth-basic*`], |
| blocking: true, |
| }).addEventListener('authrequired', (e) => { |
| e.setCredentials({ |
| username: expectedUsername, |
| password: expectedPassword |
| }); |
| }); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnAuthRequiredEventName)); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| |
| // Check that the injecting the credentials through WebRequest produces a |
| // successful navigation. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| const GURL& kAuthBasicUrl = |
| embedded_https_test_server().GetURL("/auth-basic?password=pass"); |
| web_view_guest->NavigateGuest(kAuthBasicUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kAuthBasicUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Check that the injecting the wrong credentials through WebRequest produces |
| // an error. |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| const GURL& kAuthBasicUrl = |
| embedded_https_test_server().GetURL("/auth-basic?password=badpass"); |
| web_view_guest->NavigateGuest(kAuthBasicUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kAuthBasicUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| // The auth request fails but keeps retrying until this error is produced. |
| // TODO(crbug.com/40942953): The error produced here should be |
| // authentication related. |
| EXPECT_EQ(net::Error::ERR_TOO_MANY_RETRIES, |
| navigation_observer.last_net_error_code()); |
| EXPECT_FALSE(navigation_observer.last_navigation_succeeded()); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ExecuteScript) { |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicy( |
| network::mojom::PermissionsPolicyFeature::kControlledFrame, |
| /*self=*/true, |
| /*origins=*/{})) |
| .AddHtml("/execute_script.input.js", |
| "document.body.style.backgroundColor = 'red';") |
| .BuildBundle(); |
| app->TrustSigningKey(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| |
| // Verify that executeScript() using JS code can change the background color. |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundCode(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| |
| // Verify that executeScript() using a JS file changes the background color. |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundFile(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInDataIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL https_url = embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, https_url)); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const src = '<!DOCTYPE html><p>data: URL</p>'; |
| const url = `data:text/html;base64,${btoa(src)}`; |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = url; |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 1); |
| ASSERT_NE(iframe, nullptr); |
| |
| ASSERT_FALSE(CreateControlledFrame(iframe, https_url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInSandboxedIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL https_url = embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, https_url)); |
| |
| ASSERT_TRUE( |
| ExecJs(app_frame, content::JsReplace(R"( |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = $1; |
| f.sandbox = 'allow-scripts'; // for EvalJs |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )", |
| url_info.origin().Serialize()))); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 1); |
| ASSERT_NE(iframe, nullptr); |
| |
| EXPECT_EQ(content::WebExposedIsolationLevel::kNotIsolated, |
| iframe->GetWebExposedIsolationLevel()); |
| EXPECT_EQ(false, EvalJs(iframe, "window.crossOriginIsolated")); |
| EXPECT_EQ("null", EvalJs(iframe, "window.origin")); |
| |
| ASSERT_FALSE(CreateControlledFrame(iframe, https_url)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInSrcdocIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const noopPolicy = trustedTypes.createPolicy("policy", { |
| createHTML: (string) => string, |
| }); |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.srcdoc = noopPolicy.createHTML('<!DOCTYPE html><p>srcdoc iframe</p>'); |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_NE(iframe, nullptr); |
| |
| // Despite srcdoc iframes being same-origin, creating the <controlledframe> |
| // fails because AvailabilityCheck looks at the frame's scheme as well as |
| // its isolation level. No other IsolatedContext API does this, but it makes |
| // sense for <controlledframe> because it's not a purely JS-based API that |
| // will be blocked through CSP. |
| ASSERT_FALSE(CreateControlledFrame( |
| iframe, embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, DisabledInBlobIframe) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const blob = new Blob(['<!DOCTYPE html><p>blob html page</p>'], { |
| type: 'text/html' |
| }); |
| const url = URL.createObjectURL(blob); |
| new Promise(resolve => { |
| const f = document.createElement('iframe'); |
| f.src = url; |
| f.addEventListener('load', resolve); |
| document.body.appendChild(f); |
| }); |
| )")); |
| content::RenderFrameHost* iframe = ChildFrameAt(app_frame, 0); |
| ASSERT_NE(iframe, nullptr); |
| |
| // As with srcdoc iframes, is blocked due to AvailabilityCheck verifying |
| // the frame's scheme as well as its isolation level. |
| ASSERT_FALSE(CreateControlledFrame( |
| iframe, embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, ElementHasExpectedProperties) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| std::string expected_properties_json = |
| ReadTestDataFile(kExpectedPropertiesJsonPath); |
| std::optional<base::Value> expected_properties = base::JSONReader::Read( |
| expected_properties_json, base::JSON_ALLOW_COMMENTS); |
| ASSERT_TRUE(expected_properties.has_value()); |
| |
| content::EvalJsResult result = EvalJs(app_frame, R"( |
| // Collect every property from the <controlledframe> element up the |
| // prototype chain until HTMLElement. |
| const methods = []; |
| let clazz = document.querySelector('controlledframe'); |
| while (clazz.constructor.name !== 'HTMLElement') { |
| methods.push(...Object.getOwnPropertyNames(clazz)); |
| clazz = Object.getPrototypeOf(clazz); |
| } |
| [...new Set(methods).values()].sort() |
| )"); |
| |
| EXPECT_EQ(result, expected_properties.value()); |
| } |
| |
| // This and related tests are based on a WebView test at: |
| // //extensions/test/data/web_view/no_internal_calls_to_user_code/main.js |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsBasic) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| ASSERT_THAT(EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = document.savedCreateElement('controlledframe'); |
| frame.src = 'data:text/html,<body>Guest</body>'; |
| frame.savedAddEventListener('loadabort', reject); |
| frame.savedAddEventListener('loadstop', resolve); |
| document.body.savedAppendChild(frame); |
| }); |
| )"), |
| content::EvalJsResult::IsOk()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsSetOnEventProperty) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| ASSERT_THAT(EvalJs(app_frame, R"( |
| const frame = document.savedCreateElement('controlledframe'); |
| frame.onloadstop = () => {}; |
| frame.onloadstop = () => {}; |
| )"), |
| content::EvalJsResult::IsOk()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsGetSetAttributes) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| EXPECT_EQ(kEvalSuccessStr, EvalJs(app_frame, |
| R"( |
| new Promise((resolve, reject) => { |
| const assertEq = function(expected, actual) { |
| if (expected != actual) { |
| reject(`expected ${expected} got ${actual}`); |
| } |
| } |
| |
| const frame = new HTMLControlledFrameElement(); |
| const url = 'data:text/html,<body>Guest</body>'; |
| frame.src = url; |
| assertEq(url, frame.src); |
| |
| frame.autosize = true; |
| assertEq(true, frame.autosize); |
| frame.autosize = false; |
| assertEq(false, frame.autosize); |
| |
| frame.maxheight = 123; |
| assertEq(123, frame.maxheight); |
| frame.maxheight = undefined; |
| assertEq(0, frame.maxheight); |
| |
| var name = 'my-frame'; |
| frame.name = name; |
| assertEq(name, frame.name); |
| frame.name = undefined; |
| assertEq('', frame.name); |
| resolve('SUCCESS'); |
| }); |
| )")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsBackForward) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| ASSERT_THAT(EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = new HTMLControlledFrameElement(); |
| // The back and forward methods are implemented in terms of go. Make sure |
| // they don't call an overwritten version. |
| frame.go = makeUnreached(); |
| frame.back(); |
| frame.forward(); |
| resolve(); |
| }); |
| )"), |
| content::EvalJsResult::IsOk()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsFocus) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| ASSERT_THAT(EvalJs(app_frame, R"( |
| new Promise((resolve, reject) => { |
| const frame = document.savedCreateElement('controlledframe'); |
| frame.src = 'data:text/html,<body>Guest</body>'; |
| frame.savedAddEventListener('loadabort', reject); |
| frame.savedAddEventListener('loadstop', () => { |
| frame.focus(); |
| resolve(); |
| }); |
| document.body.savedAppendChild(frame); |
| }); |
| )"), |
| content::EvalJsResult::IsOk()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, MangledJsWebRequest) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| ASSERT_TRUE(SetUseMangledJs(app_frame)); |
| |
| GURL url = embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_THAT(EvalJs(app_frame, content::JsReplace(R"( |
| new Promise((resolve, reject) => { |
| const frame = document.savedCreateElement('controlledframe'); |
| frame.src = $1; |
| frame.savedAddEventListener('loadabort', reject); |
| frame.savedAddEventListener('loadstop', () => { |
| frame.request.createWebRequestInterceptor({ |
| urlPatterns: ['*://*/*'], |
| includeHeaders: 'all', |
| }).addEventListener('completed', (e) => { |
| resolve(); |
| }); |
| frame.reload(); |
| }); |
| document.body.savedAppendChild(frame); |
| }); |
| )", |
| url)), |
| content::EvalJsResult::IsOk()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, LogMessage_Partition) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| auto* app_web_contents = content::WebContents::FromRenderFrameHost(app_frame); |
| content::WebContentsConsoleObserver console_observer(app_web_contents); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| const cf = document.querySelector('controlledframe'); |
| cf.partition = 'in_memory'; |
| )")); |
| |
| ASSERT_EQ(1UL, console_observer.messages().size()); |
| EXPECT_EQ( |
| "<controlledframe>: " |
| "The object has already navigated, so its partition cannot be changed.", |
| console_observer.GetMessageAt(0)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, LogMessage_Abort) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| auto* app_web_contents = content::WebContents::FromRenderFrameHost(app_frame); |
| content::WebContentsConsoleObserver console_observer(app_web_contents); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| ASSERT_TRUE(ExecJs(app_frame, R"( |
| new Promise((resolve) => { |
| const cf = document.querySelector('controlledframe'); |
| cf.addEventListener('loadabort', resolve); |
| cf.src = 'chrome://flags'; |
| }); |
| )")); |
| |
| ASSERT_EQ(1UL, console_observer.messages().size()); |
| EXPECT_EQ( |
| "<controlledframe>: " |
| "The load has aborted with error -301: ERR_DISALLOWED_URL_SCHEME." |
| " url: chrome://flags/", |
| console_observer.GetMessageAt(0)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, Histograms) { |
| base::HistogramTester histogram_tester; |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| histogram_tester.ExpectUniqueSample( |
| "GuestView.GuestViewCreated", |
| guest_view::GuestViewHistogramValue::kControlledFrame, 0); |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", |
| blink::mojom::WebFeature::kHTMLControlledFrameElement, 0); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| // We should have created a Controlled Frame, and should not have records for |
| // any other guest view type (`ExpectUniqueSample` guarantees both of these). |
| histogram_tester.ExpectUniqueSample( |
| "GuestView.GuestViewCreated", |
| guest_view::GuestViewHistogramValue::kControlledFrame, 1); |
| histogram_tester.ExpectBucketCount( |
| "Blink.UseCounter.Features", |
| blink::mojom::WebFeature::kHTMLControlledFrameElement, 1); |
| } |
| |
| class ControlledFrameWebSocketApiTest : public ControlledFrameApiTest { |
| public: |
| void SetUpOnMainThread() override { |
| ControlledFrameApiTest::SetUpOnMainThread(); |
| websocket_test_server_.AddDefaultHandlers(GetChromeTestDataDir()); |
| net::test_server::InstallDefaultWebSocketHandlers(&websocket_test_server_); |
| ASSERT_TRUE(websocket_test_server_.Start()); |
| } |
| |
| net::EmbeddedTestServer& websocket_test_server() { |
| return websocket_test_server_; |
| } |
| |
| GURL GetWebSocketUrl(const std::string& path) const { |
| return net::test_server::GetWebSocketURL(websocket_test_server_, path); |
| } |
| |
| private: |
| net::EmbeddedTestServer websocket_test_server_{ |
| net::EmbeddedTestServer ::Type::TYPE_HTTP}; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameWebSocketApiTest, WebSocketIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const GURL& kOriginalControlledFrameUrl = |
| embedded_https_test_server().GetURL("/index.html"); |
| ASSERT_TRUE(CreateControlledFrame(app_frame, kOriginalControlledFrameUrl)); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| // Use Web Sockets before installing a WebRequest event listener to verify |
| // that it works inside of the Controlled Frame. |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| content::WebContents* guest_web_contents = web_view_guest->web_contents(); |
| GURL::Replacements http_scheme_replacement; |
| http_scheme_replacement.SetSchemeStr("http"); |
| const GURL kWebSocketConnectCheckUrl = |
| websocket_test_server() |
| .GetURL("/websocket/connect_check.html") |
| .ReplaceComponents(http_scheme_replacement); |
| { |
| content::TitleWatcher title_watcher(guest_web_contents, u"PASS"); |
| title_watcher.AlsoWaitForTitle(u"FAIL"); |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kWebSocketConnectCheckUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kWebSocketConnectCheckUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(u"PASS", title_watcher.WaitAndGetTitle()); |
| } |
| |
| { |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kOriginalControlledFrameUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kOriginalControlledFrameUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| } |
| |
| // Set up a WebRequest event listener that cancels any requests to the Web |
| // Socket server. |
| EXPECT_EQ(true, content::EvalJs(app_frame, |
| R"( |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| |
| frame.request.createWebRequestInterceptor({ |
| urlPatterns: ['ws://*/*'], |
| blocking: true, |
| }).addEventListener('beforerequest', (e) => { |
| e.preventDefault(); |
| }); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| { |
| content::TitleWatcher title_watcher(guest_web_contents, u"PASS"); |
| title_watcher.AlsoWaitForTitle(u"FAIL"); |
| content::TestNavigationObserver navigation_observer( |
| guest_web_contents, |
| /*expected_number_of_navigations=*/1u); |
| web_view_guest->NavigateGuest(kWebSocketConnectCheckUrl.spec(), |
| /*navigation_handle_callback=*/{}, |
| /*force_navigation=*/false); |
| navigation_observer.WaitForNavigationFinished(); |
| EXPECT_EQ(kWebSocketConnectCheckUrl, |
| web_view_guest->GetGuestMainFrame()->GetLastCommittedURL()); |
| EXPECT_TRUE(navigation_observer.last_navigation_succeeded()); |
| EXPECT_EQ(u"FAIL", title_watcher.WaitAndGetTitle()); |
| } |
| } |
| |
| class ControlledFrameWebTransportApiTest : public ControlledFrameApiTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ControlledFrameApiTest::SetUpCommandLine(command_line); |
| webtransport_server_.SetUpCommandLine(command_line); |
| webtransport_server_.Start(); |
| } |
| |
| content::WebTransportSimpleTestServer& webtransport_server() { |
| return webtransport_server_; |
| } |
| |
| protected: |
| content::WebTransportSimpleTestServer webtransport_server_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ControlledFrameWebTransportApiTest, |
| WebTransportIsProxied) { |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| |
| auto* web_request_event_router = |
| extensions::WebRequestEventRouter::Get(profile()); |
| EXPECT_EQ(0u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| // Use WebTransport before installing a WebRequest event listener to verify |
| // that it works inside of the Controlled Frame. |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| EXPECT_EQ(true, content::EvalJs( |
| web_view_guest->GetGuestMainFrame(), |
| content::JsReplace( |
| R"( |
| (async function() { |
| const url = 'https://localhost:' + $1 + '/echo_test'; |
| try { |
| const transport = new WebTransport(url); |
| await transport.ready; |
| } catch (e) { |
| console.log(url + ': ' + e.name + ': ' + e.message); |
| return false; |
| } |
| return true; |
| })(); |
| )", |
| webtransport_server().server_address().port()))); |
| |
| // Set up a WebRequest event listener that cancels any requests to the |
| // WebTransport server. |
| EXPECT_EQ(true, content::EvalJs(app_frame, |
| R"( |
| let cancelRequest = false; |
| (function() { |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| if (!frame || !frame.request) { |
| return false; |
| } |
| frame.request.createWebRequestInterceptor({ |
| urlPatterns: ['https://localhost/*'], |
| blocking: true, |
| }).addEventListener('beforerequest', (e) => { |
| e.preventDefault(); |
| }); |
| return true; |
| })(); |
| )")); |
| EXPECT_EQ(1u, web_request_event_router->GetListenerCountForTesting( |
| profile(), kWebRequestOnBeforeRequestEventName)); |
| |
| EXPECT_EQ(false, content::EvalJs( |
| web_view_guest->GetGuestMainFrame(), |
| content::JsReplace( |
| R"( |
| (async function() { |
| cancelRequest = true; |
| const url = 'https://localhost:' + $1 + '/echo_test'; |
| try { |
| const transport = new WebTransport(url); |
| await transport.ready; |
| } catch (e) { |
| console.log(url + ': ' + e.name + ': ' + e.message); |
| return false; |
| } |
| return true; |
| })(); |
| )", |
| webtransport_server().server_address().port()))); |
| } |
| |
| class ControlledFramePromiseApiTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface<const char*> {}; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFramePromiseApiTest, PromiseAPIs) { |
| std::unique_ptr<web_app::ScopedProxyIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicy( |
| network::mojom::PermissionsPolicyFeature::kControlledFrame, |
| /*self=*/true, |
| /*origins=*/{})) |
| .AddFolderFromDisk("/", "web_apps/simple_isolated_app") |
| .BuildAndStartProxyServer(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = |
| OpenApp(url_info.app_id(), "/controlled_frame_api_test.html"); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, app->proxy_server().GetURL("/controlled_frame.html"))); |
| |
| EXPECT_EQ("SUCCESS", |
| content::EvalJs(app_frame, content::JsReplace(R"( |
| const frame = document.getElementsByTagName('controlledframe')[0]; |
| testAPI(frame, $1); |
| )", |
| GetParam()))); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(PromiseAPIs, |
| ControlledFramePromiseApiTest, |
| testing::ValuesIn(kControlledFramePromiseApiMethods)); |
| |
| class ControlledFrameServiceWorkerTest |
| : public extensions::ServiceWorkerBasedBackgroundTest { |
| public: |
| ControlledFrameServiceWorkerTest(const ControlledFrameServiceWorkerTest&) = |
| delete; |
| ControlledFrameServiceWorkerTest& operator=( |
| const ControlledFrameServiceWorkerTest&) = delete; |
| |
| void SetUpOnMainThread() override { |
| extensions::ServiceWorkerBasedBackgroundTest::SetUpOnMainThread(); |
| embedded_https_test_server().ServeFilesFromSourceDirectory( |
| GetChromeTestDataDir()); |
| ASSERT_TRUE(embedded_https_test_server().Start()); |
| } |
| |
| protected: |
| ControlledFrameServiceWorkerTest() = default; |
| |
| ~ControlledFrameServiceWorkerTest() override = default; |
| |
| base::test::ScopedFeatureList feature_list; |
| }; |
| |
| // This test ensures that loading an extension Service Worker does not cause a |
| // crash, and that Controlled Frame is not allowed in the Service Worker |
| // context. For more details, see https://crbug.com/1462384. |
| // This test is the same as ServiceWorkerBasedBackgroundTest.Basic. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameServiceWorkerTest, PRE_Basic) { |
| ExtensionTestMessageListener newtab_listener("CREATED"); |
| newtab_listener.set_failure_message("CREATE_FAILED"); |
| ExtensionTestMessageListener worker_listener("WORKER_RUNNING"); |
| worker_listener.set_failure_message("NON_WORKER_SCOPE"); |
| const extensions::Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII( |
| "service_worker/worker_based_background/basic")); |
| ASSERT_TRUE(extension); |
| const extensions::ExtensionId extension_id = extension->id(); |
| EXPECT_TRUE(worker_listener.WaitUntilSatisfied()); |
| |
| const GURL url = |
| embedded_https_test_server().GetURL("/extensions/test_file.html"); |
| content::WebContents* new_web_contents = |
| extensions::browsertest_util::AddTab(browser(), url); |
| EXPECT_TRUE(new_web_contents); |
| EXPECT_TRUE(newtab_listener.WaitUntilSatisfied()); |
| |
| // Service Worker extension does not have ExtensionHost. |
| EXPECT_FALSE(process_manager()->GetBackgroundHostForExtension(extension_id)); |
| } |
| |
| // After browser restarts, this test step ensures that opening a tab fires |
| // tabs.onCreated event listener to the extension without explicitly loading the |
| // extension. This is because the extension registered a listener before browser |
| // restarted in PRE_Basic. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameServiceWorkerTest, Basic) { |
| ExtensionTestMessageListener newtab_listener("CREATED"); |
| newtab_listener.set_failure_message("CREATE_FAILED"); |
| const GURL url = |
| embedded_https_test_server().GetURL("/extensions/test_file.html"); |
| content::WebContents* new_web_contents = |
| extensions::browsertest_util::AddTab(browser(), url); |
| EXPECT_TRUE(new_web_contents); |
| EXPECT_TRUE(newtab_listener.WaitUntilSatisfied()); |
| } |
| |
| class ControlledFrameNotAvailableChannelTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface<version_info::Channel> { |
| protected: |
| ControlledFrameNotAvailableChannelTest() |
| : ControlledFrameApiTest(/*channel=*/GetParam(), |
| /*feature_setting=*/FeatureSetting::ENABLED, |
| /*flag_setting=*/FlagSetting::CONTROLLED_FRAME) { |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(ControlledFrameNotAvailableChannels, |
| ControlledFrameNotAvailableChannelTest, |
| testing::Values(version_info::Channel::STABLE, |
| version_info::Channel::BETA, |
| version_info::Channel::DEV, |
| version_info::Channel::CANARY, |
| version_info::Channel::DEFAULT)); |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameNotAvailableChannelTest, Test) { |
| // Test if Controlled Frame is not available. |
| const GURL start_url("https://app.site.test/example/index"); |
| InstallPWA(start_url); |
| content::WebContents* app_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| ASSERT_FALSE(CreateControlledFrame( |
| app_contents->GetPrimaryMainFrame(), |
| embedded_https_test_server().GetURL("/index.html"))); |
| } |
| |
| class ControlledFrameAvailabilityTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface< |
| ::std::tuple<version_info::Channel, FeatureSetting, FlagSetting>> { |
| protected: |
| ControlledFrameAvailabilityTest() |
| : ControlledFrameApiTest( |
| /*channel=*/std::get<0>(GetParam()), |
| /*feature_setting=*/std::get<1>(GetParam()), |
| /*flag_setting=*/std::get<2>(GetParam())) {} |
| |
| ~ControlledFrameAvailabilityTest() override = default; |
| |
| // |DetermineExpectedState| derives the expected enabling status based on |
| // the channel, feature, and flag inputs. |
| // |
| // Ideally, when we set a feature to enabled or disabled in the test setup, |
| // we would be altering that feature's default setting for the purposes of |
| // the test. However, ScopedFeatureList doesn't enable or disable a feature |
| // via defaults but instead by overrides. As a result, any feature that's |
| // enabled or disabled by ScopedFeatureList will appear as an override. |
| bool DetermineExpectedState() { |
| return feature_setting() != FeatureSetting::DISABLED; |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* */, |
| ControlledFrameAvailabilityTest, |
| /* Per-channel tests examine the extensions-based availability system. */ |
| testing::Combine( |
| /*channel=*/testing::Values(version_info::Channel::STABLE, |
| version_info::Channel::BETA, |
| version_info::Channel::DEV, |
| version_info::Channel::CANARY), |
| /*feature=*/ |
| testing::Values(FeatureSetting::NONE, |
| FeatureSetting::DISABLED, |
| FeatureSetting::ENABLED), |
| /*flag=*/ |
| testing::Values(FlagSetting::NONE, |
| FlagSetting::EXPERIMENTAL, |
| FlagSetting::CONTROLLED_FRAME))); |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameAvailabilityTest, Verify) { |
| const bool expected_enabled = DetermineExpectedState(); |
| EXPECT_EQ(expected_enabled, |
| base::FeatureList::IsEnabled(blink::features::kControlledFrame)); |
| |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const bool actual_enabled = CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html")); |
| EXPECT_EQ(expected_enabled, actual_enabled) |
| << "Test failure for case: " << ConfigToString(); |
| |
| // Uncomment for debugging information: |
| // DLOG(ERROR) << ConfigToString(); |
| // DLOG(ERROR) << "expected_enabled=" << expected_enabled |
| // << "; actual_enabled=" << actual_enabled; |
| |
| if (expected_enabled && actual_enabled) { |
| auto* web_view_guest = GetWebViewGuest(app_frame); |
| EXPECT_EQ(kEvalSuccessStr, SetBackgroundColorToWhite(web_view_guest)); |
| EXPECT_EQ(kEvalSuccessStr, ExecuteScriptRedBackgroundCode(app_frame)); |
| EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); |
| } |
| } |
| |
| class ControlledFrameAvailabilityAdminPolicyTest |
| : public ControlledFrameApiTest, |
| public testing::WithParamInterface<ContentSetting> {}; |
| |
| IN_PROC_BROWSER_TEST_P(ControlledFrameAvailabilityAdminPolicyTest, |
| VerifyPolicy) { |
| // Get the expected content setting and set it up. |
| const ContentSetting content_setting = GetParam(); |
| |
| bool expected_enabled = |
| content_setting != ContentSetting::CONTENT_SETTING_BLOCK; |
| |
| HostContentSettingsMapFactory::GetForProfile(profile()) |
| ->SetDefaultContentSetting(ContentSettingsType::CONTROLLED_FRAME, |
| content_setting); |
| |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const bool actual_enabled = CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html")); |
| EXPECT_EQ(expected_enabled, actual_enabled); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* */, |
| ControlledFrameAvailabilityAdminPolicyTest, |
| /* Per-channel tests examine the extensions-based availability system. */ |
| testing::Values(ContentSetting::CONTENT_SETTING_DEFAULT, |
| ContentSetting::CONTENT_SETTING_ALLOW, |
| ContentSetting::CONTENT_SETTING_BLOCK)); |
| |
| class ControlledFrameRequestHeaderTest : public ControlledFrameTestBase { |
| public: |
| [[nodiscard]] bool SetUserAgentAndAwaitReload(content::RenderFrameHost* frame, |
| const std::string& user_agent) { |
| const std::string kRemoveUserAgentAndReload = R"( |
| new Promise((resolve, reject) => { |
| const controlledframe = document.getElementsByTagName('controlledframe')[0]; |
| if (!('src' in controlledframe)) { |
| reject('FAIL'); |
| return; |
| } |
| controlledframe.addEventListener('loadstop', resolve); |
| controlledframe.addEventListener('loadabort', reject); |
| // |setUserAgentOverride| should automatically reload. |
| controlledframe.setUserAgentOverride($1); |
| }); |
| )"; |
| return ExecJs(frame, |
| content::JsReplace(kRemoveUserAgentAndReload, user_agent)); |
| } |
| |
| [[nodiscard]] bool SetClientHintsUABrandEnabled( |
| content::RenderFrameHost* frame, |
| bool enable) { |
| const std::string kToggleClientHintsBrandAndReload = R"( |
| new Promise((resolve, reject) => { |
| const controlledframe = document.getElementsByTagName('controlledframe')[0]; |
| if (!('src' in controlledframe)) { |
| reject('FAIL'); |
| return; |
| } |
| controlledframe.addEventListener('loadstop', resolve); |
| controlledframe.addEventListener('loadabort', reject); |
| |
| // |setClientHintsUABrandEnabled| should automatically reload. |
| controlledframe.setClientHintsUABrandEnabled($1); |
| }); |
| )"; |
| return ExecJs(frame, |
| content::JsReplace(kToggleClientHintsBrandAndReload, enable)); |
| } |
| |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| if (request.relative_url != "/index.html") { |
| return; |
| } |
| ASSERT_TRUE(request.headers.contains("User-Agent")); |
| last_seen_ua_ = request.headers.at("User-Agent"); |
| ASSERT_TRUE(request.headers.contains("Sec-CH-UA")); |
| last_seen_sec_ch_ua_ = request.headers.at("Sec-CH-UA"); |
| } |
| |
| const std::string& last_seen_ua() const { return last_seen_ua_; } |
| const std::string& last_seen_sec_ch_ua() const { |
| return last_seen_sec_ch_ua_; |
| } |
| |
| private: |
| std::string last_seen_ua_; |
| std::string last_seen_sec_ch_ua_; |
| }; |
| |
| // Verifies that `setUserAgentOverride` works as expected, and that default |
| // Sec-CH-UA includes "ControlledFrame" brand. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameRequestHeaderTest, |
| HasDefaultCHUABrandWithUAOverride) { |
| embedded_https_test_server().RegisterRequestMonitor( |
| base::BindRepeating(&ControlledFrameRequestHeaderTest::MonitorRequest, |
| base::Unretained(this))); |
| |
| StartContentServer("web_apps/simple_isolated_app"); |
| |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), HasSubstr("ControlledFrame")); |
| |
| ASSERT_TRUE(SetUserAgentAndAwaitReload(app_frame, "foobar")); |
| EXPECT_EQ(last_seen_ua(), "foobar"); |
| EXPECT_THAT(last_seen_sec_ch_ua(), HasSubstr("ControlledFrame")); |
| |
| // Passing an empty string should reset the UA value. |
| ASSERT_TRUE(SetUserAgentAndAwaitReload(app_frame, "")); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), HasSubstr("ControlledFrame")); |
| } |
| |
| // `setClientHintsUABrandEnabled` toggles the `Sec-CH-UA` headers to Chrome |
| // default or Controlled Frame default. |
| IN_PROC_BROWSER_TEST_F(ControlledFrameRequestHeaderTest, |
| SetClientHintsUABrandEnabled) { |
| embedded_https_test_server().RegisterRequestMonitor( |
| base::BindRepeating(&ControlledFrameRequestHeaderTest::MonitorRequest, |
| base::Unretained(this))); |
| |
| StartContentServer("web_apps/simple_isolated_app"); |
| |
| web_app::IsolatedWebAppUrlInfo url_info = |
| CreateAndInstallEmptyApp(web_app::ManifestBuilder()); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| ASSERT_TRUE(CreateControlledFrame( |
| app_frame, embedded_https_test_server().GetURL("/index.html"))); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), HasSubstr("ControlledFrame")); |
| |
| // Disables the "ControlledFrame" brand in CH-UA. |
| ASSERT_TRUE(SetClientHintsUABrandEnabled(app_frame, false)); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), Not(HasSubstr("ControlledFrame"))); |
| |
| // Setting a custom UA should not reset the CH-UA. |
| ASSERT_TRUE(SetUserAgentAndAwaitReload(app_frame, "foobar")); |
| EXPECT_EQ(last_seen_ua(), "foobar"); |
| EXPECT_THAT(last_seen_sec_ch_ua(), Not(HasSubstr("ControlledFrame"))); |
| |
| // Passing an empty string should reset the UA value. |
| ASSERT_TRUE(SetUserAgentAndAwaitReload(app_frame, "")); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), Not(HasSubstr("ControlledFrame"))); |
| |
| // Re-enables the "ControlledFrame" brand in the CH-UA. |
| ASSERT_TRUE(SetClientHintsUABrandEnabled(app_frame, true)); |
| EXPECT_EQ(last_seen_ua(), embedder_support::GetUserAgent()); |
| EXPECT_THAT(last_seen_sec_ch_ua(), HasSubstr("ControlledFrame")); |
| } |
| |
| } // namespace controlled_frame |