blob: 6521ad7e8db49854480c0f9ab32be0e2fc4ac7d4 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/files/file_path.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "components/version_info/channel.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/ui_test_utils.h"
#endif
namespace extensions {
namespace {
static constexpr char kManifestStub[] = R"({
"name": "Test",
"version": "0.1",
"manifest_version": 3,
"web_accessible_resources": [
{
"resources": [ "dynamic.html" ],
"matches": [ "<all_urls>" ],
"use_dynamic_url": true
},
{
"resources": [ "static.html" ],
"matches": [ "<all_urls>" ]
}
]
})";
static constexpr char kFetchResourceScriptTemplate[] = R"(
// Verify that the web accessible resource can be fetched.
async function test(title, filename, useDynamicUrl, isAllowed) {
return new Promise(async resolve => {
const dynamicUrl = `chrome-extension://%s/${filename}`;
const staticUrl = `chrome-extension://%s/${filename}`;
const url = useDynamicUrl ? dynamicUrl : staticUrl;
// Fetch and verify the contents of fetched web accessible resources.
const verifyFetch = (actual) => {
if (isAllowed == (filename == actual)) {
resolve();
} else {
reject(`${title}. Expected: ${filename}. Actual: ${actual}`);
}
};
fetch(url)
.then(result => result.text())
.catch(error => verifyFetch(error))
.then(text => verifyFetch(text));
});
}
// Run tests with list example: [[title, filename, useDynamicUrl, isAllowed]].
const testCases = [%s];
const tests = testCases.map(testCase => test(...testCase));
Promise.all(tests).then(response => true);
)";
// Exercise web accessible resources.
class WebAccessibleResourcesBrowserTest : public ExtensionBrowserTest {
public:
WebAccessibleResourcesBrowserTest() = default;
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
EXPECT_TRUE(embedded_test_server()->Start());
}
private:
// TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
extensions::ScopedTestMV2Enabler mv2_enabler_;
};
// If `use_dynamic_url` is set to true in manifest.json, then the associated web
// accessible resource(s) can only be loaded using the dynamic url. Loading with
// the static url containing the extension id won't work.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
UseDynamicUrlInFetch) {
// Load extension.
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifestStub);
extension_dir.WriteFile(FILE_PATH_LITERAL("dynamic.html"), "dynamic.html");
extension_dir.WriteFile(FILE_PATH_LITERAL("static.html"), "static.html");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
// Navigate to a test page and get the web contents.
base::FilePath test_page;
GURL gurl = embedded_test_server()->GetURL("example.com", "/simple.html");
auto* web_contents = GetActiveWebContents();
ASSERT_TRUE(content::NavigateToURL(web_contents, gurl));
std::string script =
base::StringPrintf(kFetchResourceScriptTemplate,
extension->guid().c_str(), extension->id().c_str(), R"(
["Load a static resource with a dynamic url", 'static.html', true, true],
["Load a static resource with a static url", 'static.html', false, true],
["Load dynamic resource with a dynamic url", 'dynamic.html', true, true],
["Load dynamic resource with a static url", 'dynamic.html', false, false],
)");
ASSERT_TRUE(content::EvalJs(web_contents, script).ExtractBool());
}
// Exercise these resources being used in iframes in a web page. The navigation
// flow goes through a different path than resource fetching.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
UseDynamicUrlInIframe) {
// Load an extension that has one web accessible resource.
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifestStub);
extension_dir.WriteFile(FILE_PATH_LITERAL("dynamic.html"),
"dynamic resource");
extension_dir.WriteFile(FILE_PATH_LITERAL("static.html"), "static resource");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
EXPECT_TRUE(extension);
auto navigate = [&](const GURL& target, const GURL& commit,
const std::string& expected) {
// Navigate the main frame with a browser initiated navigation to a blank
// web page. This should succeed.
const GURL gurl = embedded_test_server()->GetURL("/iframe_blank.html");
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_TRUE(content::NavigateToURL(web_contents, gurl));
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* iframe = content::ChildFrameAt(main_frame, 0);
EXPECT_TRUE(iframe);
// Navigate the iframe with a renderer initiated navigation to a web
// accessible resource. This should succeed.
content::TestNavigationObserver nav_observer(web_contents);
EXPECT_TRUE(content::NavigateIframeToURL(web_contents, "test", target));
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(net::OK, nav_observer.last_net_error_code());
iframe = content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
EXPECT_EQ(commit, iframe->GetLastCommittedURL());
EXPECT_EQ(expected, EvalJs(iframe, "document.body.innerText;"));
};
static struct {
const char* title;
const GURL target;
const GURL commit;
const std::string expected;
} test_cases[] = {
{
"Static",
extension->GetResourceURL("static.html"),
extension->GetResourceURL("static.html"),
"static resource",
},
{
"Dynamic",
Extension::GetResourceURL(extension->dynamic_url(), "dynamic.html"),
extension->GetResourceURL("dynamic.html"),
"dynamic resource",
},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(base::StringPrintf("Error: '%s'", test_case.title));
navigate(test_case.target, test_case.commit, test_case.expected);
}
}
// Tests that navigating a main frame via location.href works if and only if
// the target resource is accessible to the main frame.
// Regression test for https://crbug.com/374503948.
IN_PROC_BROWSER_TEST_F(
WebAccessibleResourcesBrowserTest,
MainFrameLocationHrefUpdatesAreSubjectToAccessibleResources) {
static constexpr char kManifest[] =
R"({
"name": "Test",
"version": "0.1",
"manifest_version": 3,
"web_accessible_resources": [
{
"resources": [ "accessible.html" ],
"matches": [ "http://trusted.example/*" ],
"use_dynamic_url": true
}
]
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("accessible.html"), "accessible");
test_dir.WriteFile(FILE_PATH_LITERAL("inaccessible.html"), "inaccessible");
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
const GURL dynamic_accessible_url =
Extension::GetResourceURL(extension->dynamic_url(), "accessible.html");
const GURL static_accessible_url =
extension->GetResourceURL("accessible.html");
const GURL dynamic_inaccessible_url =
Extension::GetResourceURL(extension->dynamic_url(), "inaccessible.html");
const GURL static_inaccessible_url =
extension->GetResourceURL("inaccessible.html");
const GURL trusted_site =
embedded_test_server()->GetURL("trusted.example", "/simple.html");
const GURL untrusted_site =
embedded_test_server()->GetURL("untrusted.example", "/simple.html");
const GURL invalid_extension_url(kExtensionInvalidRequestURL);
// Each test case will:
// * Navigate to an initial site.
// * Try to navigate that page to a target url with `location.replace();` this
// is a renderer-initiated navigation, and should be limited to
// web-accessible resource checks.
// * Verify where the navigation reached.
static struct {
// The url to navigate the browser tab to.
GURL site_url;
// The url to navigate to via `document.location.replace()` (an extension
// resource).
GURL target_url;
// The final url we expect; this should either be the static url of the
// extension resource or chrome-extension://invalid.
GURL final_url;
// If non-null, the `document.body.innerHTML` we expect for the final page.
const char* body_content;
} test_cases[] = {
{trusted_site, dynamic_accessible_url, static_accessible_url,
"accessible"},
{trusted_site, dynamic_inaccessible_url, invalid_extension_url, nullptr},
{trusted_site, static_accessible_url, invalid_extension_url, nullptr},
{trusted_site, static_inaccessible_url, invalid_extension_url, nullptr},
{untrusted_site, dynamic_accessible_url, invalid_extension_url, nullptr},
{untrusted_site, dynamic_inaccessible_url, invalid_extension_url,
nullptr},
{untrusted_site, static_accessible_url, invalid_extension_url, nullptr},
{untrusted_site, static_inaccessible_url, invalid_extension_url, nullptr},
};
content::WebContents* web_contents = GetActiveWebContents();
for (const auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message() << "Site URL: " << test_case.site_url
<< "Target URL: " << test_case.target_url
<< "Final URL: " << test_case.final_url);
ASSERT_TRUE(content::NavigateToURL(web_contents, test_case.site_url));
EXPECT_EQ(test_case.site_url, web_contents->GetLastCommittedURL());
ASSERT_TRUE(content::ExecJs(
web_contents, base::StringPrintf("document.location.replace('%s')",
test_case.target_url.spec().c_str())));
content::WaitForLoadStop(web_contents);
EXPECT_EQ(test_case.final_url, web_contents->GetLastCommittedURL());
if (test_case.body_content) {
EXPECT_EQ(test_case.body_content,
content::EvalJs(web_contents, "document.body.innerText"));
}
}
}
#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/390687767): Port to desktop Android. Currently the redirect
// doesn't happen.
// Navigate to a web page and then try to load an extension subresource.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
SubresourceReachabilityAfterServerRedirect) {
// Load extension.
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"({
"name": "test",
"version": "1",
"manifest_version": 3,
"web_accessible_resources": [{
"resources": [ "accessible.html" ],
"matches": [ "<all_urls>" ]
}]
})";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("accessible.html"),
"accessible.html");
extension_dir.WriteFile(FILE_PATH_LITERAL("inaccessible.html"),
"inaccessible.html");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
ASSERT_TRUE(extension);
GURL gurl(embedded_test_server()->GetURL("example.org", "/iframe.html"));
struct {
const char* title;
const char* filename;
net::Error error;
} test_cases[] = {
{"inaccessible", "inaccessible.html", net::ERR_BLOCKED_BY_CLIENT},
{"accessible", "accessible.html", net::OK}};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.title);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
// Navigate to a web page and then fetch the supplied subresource.
static constexpr char kScriptTemplate[] = R"(
const serverOrigin = '%s';
const resourceUrl = '%s';
// Verify that web accessible resource can be fetched.
async function run() {
return new Promise(async (resolve, reject) => {
const url = `${serverOrigin}?${resourceUrl}`;
const iframe = document.getElementById('test');
iframe.onload = event => resolve();
iframe.src = url;
});
}
run().then(response => true);
)";
GURL resource_url = extension->GetResourceURL(test_case.filename);
std::string script =
base::StringPrintf(kScriptTemplate,
embedded_test_server()
->GetURL("example.com", "/server-redirect")
.spec(),
resource_url.spec());
// Get the first child frame, which should be the only html child [iframe].
auto* active_web_contents = GetActiveWebContents();
content::RenderFrameHost* first_child =
content::ChildFrameAt(active_web_contents, 0);
// Determine if the subresource load was successful.
content::TestFrameNavigationObserver nav_observer(first_child);
ASSERT_TRUE(content::EvalJs(active_web_contents, script).ExtractBool());
nav_observer.Wait();
EXPECT_EQ(test_case.error, nav_observer.last_net_error_code());
if (nav_observer.last_net_error_code() == net::OK) {
ASSERT_TRUE(nav_observer.last_navigation_succeeded());
ASSERT_EQ(resource_url, nav_observer.last_committed_url());
}
}
}
// Server redirect to a web accessible resource whereby `matches` doesn't match.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
ServerRedirectSubresource) {
// Load extension.
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"({
"name": "test",
"version": "1",
"manifest_version": 3,
"web_accessible_resources": [{
"resources": [ "accessible.html" ],
"matches": [ "http://no.example.com/*" ]
}]
})";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("accessible.html"),
"accessible.html");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
ASSERT_TRUE(extension);
GURL gurl(embedded_test_server()->GetURL("an.example.org", "/iframe.html"));
const char* filename = "accessible.html";
net::Error error = net::ERR_BLOCKED_BY_CLIENT;
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
// Navigate to a web page and then fetch the supplied subresource.
static constexpr char kScriptTemplate[] = R"(
const serverOrigin = '%s';
const resourceUrl = '%s';
// Verify that web accessible resource can be fetched.
async function run() {
return new Promise(async (resolve, reject) => {
const url = `${serverOrigin}?${resourceUrl}`;
const iframe = document.getElementById('test');
iframe.onload = event => resolve();
iframe.src = url;
});
}
run().then(response => true);
)";
GURL resource_url = extension->GetResourceURL(filename);
std::string script =
base::StringPrintf(kScriptTemplate,
embedded_test_server()
->GetURL("an.example.com", "/server-redirect")
.spec(),
resource_url.spec());
// Get the first child frame, which should be the only html child [iframe].
auto* active_web_contents = GetActiveWebContents();
content::RenderFrameHost* first_child =
content::ChildFrameAt(active_web_contents, 0);
// Determine if the subresource load was successful.
content::TestFrameNavigationObserver nav_observer(first_child);
ASSERT_TRUE(content::EvalJs(active_web_contents, script).ExtractBool());
nav_observer.Wait();
EXPECT_EQ(error, nav_observer.last_net_error_code());
if (nav_observer.last_net_error_code() == net::OK) {
ASSERT_TRUE(nav_observer.last_navigation_succeeded());
ASSERT_EQ(resource_url, nav_observer.last_committed_url());
}
}
// Server redirect to a web accessible resource whereby `matches` doesn't match.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
ServerRedirectMainframe) {
// Load extension.
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"({
"name": "test",
"version": "1",
"manifest_version": 3,
"web_accessible_resources": [{
"resources": [ "accessible.html" ],
"matches": [ "http://no.example.com/*" ]
}]
})";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("accessible.html"),
"accessible.html");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
ASSERT_TRUE(extension);
std::string url =
base::StringPrintf("/server-redirect?%s",
extension->GetResourceURL("accessible.html").spec());
GURL gurl(embedded_test_server()->GetURL("an.example.org", url));
auto* web_contents = GetActiveWebContents();
content::TestNavigationObserver observer(web_contents);
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
observer.WaitForNavigationFinished();
EXPECT_FALSE(observer.last_navigation_succeeded());
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, observer.last_net_error_code());
}
#endif // !BUILDFLAG(IS_ANDROID)
#if !BUILDFLAG(IS_ANDROID)
// DNR, WAR, and use_dynamic_url with the extension feature. DNR does not
// currently succeed when redirecting to a resource using use_dynamic_url with
// query parameters.
// TODO(crbug.com/383366125): Port to desktop Android once chrome.runtime is
// fully ported. Right now the ExtensionTestMessageListener times out.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
DeclarativeNetRequest) {
ExtensionTestMessageListener listener("ready");
auto file_path = test_data_dir_.AppendASCII("web_accessible_resources/dnr");
const Extension* extension = LoadExtension(file_path);
ASSERT_TRUE(extension);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Navigate to a non-extension web page before beginning the test. This might
// not be needed, but it will at the very least put the tab on a known url.
{
content::WebContents* web_contents = GetActiveWebContents();
GURL gurl = embedded_test_server()->GetURL("example.com", "/simple.html");
content::TestNavigationObserver navigation_observer(web_contents);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
ASSERT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(gurl, web_contents->GetLastCommittedURL());
}
// Redirect from a webpage to a web accessible resource that has
// `use_dynamic_url` set to true. The route is from a web page through DNR,
// WAR, and on to a webpage using `use_dynamic_url`.
{
// Initialize redirection from example.com to example.org through DNR + WAR.
GURL end(embedded_test_server()->GetURL("example.org", "/empty.html"));
GURL start(
base::StringPrintf("https://example.com/url?q=%s", end.spec().c_str()));
// Navigate from within the page instead of from the Omnibox. That's because
// in manual testing, this would succeed when the url is pasted into the
// Omnibox but not when the same url is clicked from a link withing the
// page.
content::WebContents* web_contents = GetActiveWebContents();
content::TestNavigationObserver navigation_observer(web_contents);
ASSERT_TRUE(ExecJs(web_contents->GetPrimaryMainFrame(),
base::StringPrintf("window.location.href = '%s';",
start.spec().c_str())));
navigation_observer.Wait();
// Verify that the expected end url has been reached. Execution of the
// script on the `start` should redirect to `end`.
EXPECT_EQ(end, navigation_observer.last_navigation_url());
EXPECT_EQ(end, web_contents->GetLastCommittedURL());
EXPECT_EQ(net::Error::OK, navigation_observer.last_net_error_code());
EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
}
}
#endif // !BUILDFLAG(IS_ANDROID)
// Verify setting script.src from a content script that relies on web request to
// redirect to a web accessible resource. It's important to set `script.src`
// using a script so that `CanRequestResource` has `upstream_url` set to
// something other than a chrome extension.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
WebRequestRedirectFromScript) {
ExtensionTestMessageListener listener("ready");
auto file_path = test_data_dir_.AppendASCII(
"web_accessible_resources/web_request/redirect_from_script");
const Extension* extension = LoadExtension(file_path);
ASSERT_TRUE(extension);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Navigate to a non extension page.
content::WebContents* web_contents = GetActiveWebContents();
GURL gurl = embedded_test_server()->GetURL("example.com", "/empty.html");
content::TestNavigationObserver navigation_observer(web_contents);
ASSERT_TRUE(content::NavigateToURL(web_contents, gurl));
ASSERT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(gurl, web_contents->GetLastCommittedURL());
EXPECT_EQ(net::Error::OK, navigation_observer.last_net_error_code());
}
// Tests an extension using webRequest to redirect a resource included in a
// page's static html.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
WebRequestRedirectFromPage) {
ExtensionTestMessageListener listener("ready");
auto file_path = test_data_dir_.AppendASCII(
"web_accessible_resources/web_request/redirect_from_page");
const Extension* extension = LoadExtension(file_path);
ASSERT_TRUE(extension);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Navigate to a non extension page.
content::WebContents* web_contents = GetActiveWebContents();
GURL gurl = embedded_test_server()->GetURL(
"example.com", "/extensions/api_test/webrequest/script/index.html");
content::TestNavigationObserver navigation_observer(web_contents);
ASSERT_TRUE(content::NavigateToURL(web_contents, gurl));
ASSERT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(gurl, web_contents->GetLastCommittedURL());
EXPECT_EQ(net::Error::OK, navigation_observer.last_net_error_code());
}
// Succeed when DNR redirects a script to a WAR where use_dynamic_url is true.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest, DNRRedirect) {
auto file_path =
test_data_dir_.AppendASCII("web_accessible_resources/dnr/redirect");
const Extension* extension = LoadExtension(file_path);
ASSERT_TRUE(extension);
// Navigate to a non extension page.
content::WebContents* web_contents = GetActiveWebContents();
GURL gurl =
embedded_test_server()->GetURL("example.com", "/simple_with_script.html");
content::TestNavigationObserver navigation_observer(web_contents);
ASSERT_TRUE(content::NavigateToURL(web_contents, gurl));
ASSERT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(gurl, web_contents->GetLastCommittedURL());
EXPECT_EQ(net::Error::OK, navigation_observer.last_net_error_code());
auto result = EvalJs(web_contents, "document.body.textContent");
EXPECT_EQ("dnr redirect success", result.ExtractString());
}
#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/425708956): Enable this test for desktop Android.
class WebAccessibleResourcesServiceWorkerBrowserTest
: public WebAccessibleResourcesBrowserTest {
public:
WebAccessibleResourcesServiceWorkerBrowserTest() {
UseHttpsTestServer();
// Add any host names used by tests from this class to the test server's SSL
// config since tests will navigate there.
net::EmbeddedTestServer::ServerCertificateConfig cert_config;
cert_config.dns_names = {"example.com"};
embedded_test_server()->SetSSLConfig(cert_config);
}
~WebAccessibleResourcesServiceWorkerBrowserTest() override = default;
WebAccessibleResourcesServiceWorkerBrowserTest(
const WebAccessibleResourcesServiceWorkerBrowserTest&) = delete;
WebAccessibleResourcesServiceWorkerBrowserTest& operator=(
const WebAccessibleResourcesServiceWorkerBrowserTest&) = delete;
protected:
void RegisterServiceWorker(const std::string& host_name,
const std::string& worker_path,
const std::optional<std::string>& scope) {
auto* web_contents = GetActiveWebContents();
GURL url = embedded_test_server()->GetURL(
host_name, "/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));
}
};
// Test that DNR redirects to the extension's web accessible resource work when
// the page has a service worker. Unlike the WebAccessibleResourcesBrowserTest
// version, the service worker causes a renderer level redirect check for the
// web accessible resource.
// Regression test for crbug.com/375395102.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesServiceWorkerBrowserTest,
// TODO(crbug.com/429438829): Re-enable this test
DISABLED_DNRRedirect) {
// Register a service worker and navigate to a page it controls.
RegisterServiceWorker("example.com", "fetch_event_pass_through.js",
std::nullopt);
EXPECT_TRUE(NavigateToURL(
browser_window_interface(),
embedded_test_server()->GetURL("example.com",
"/service_worker/fetch_from_page.html")));
const Extension* extension = LoadExtension(test_data_dir_.AppendASCII(
"web_accessible_resources/dnr/redirect_with_initiator"));
ASSERT_TRUE(extension);
// Fetch the english page. It should be redirected to the extension's web
// accessible resource. Note: we "lose" the service worker if we attempt to
// navigate to the page instead, so a fetch is used here.
auto result =
EvalJs(GetActiveWebContents(), "fetch_from_page('/english_page.html');");
std::string expected_content =
"// Redirect with initiator's web accessible resource!";
EXPECT_TRUE(result.ExtractString().find(expected_content) !=
std::string::npos)
<< expected_content << " not found in " << result.ExtractString();
}
// TODO(crbug.com/390687767): Port to desktop Android. Currently the redirect
// doesn't happen.
// Test server redirect to a web accessible or extension resource.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
BrowserProcessRedirect) {
auto TestBrowserRedirect = [&](const char* kManifest,
const char* kHistogramName) {
// Load extension.
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("resource.html"), "resource.html");
test_dir.WriteFile(FILE_PATH_LITERAL("web_accessible_resource.html"),
"web_accessible_resource.html");
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
base::HistogramTester histogram_tester;
// Test extension resource accessibility.
auto server_redirect = [&](int expect_net_error, const char* resource,
bool is_accessible) {
GURL gurl = embedded_test_server()->GetURL(
"example.com",
base::StringPrintf(
"/server-redirect?%s",
extension->GetResourceURL(resource).spec().c_str()));
auto* web_contents = GetActiveWebContents();
content::TestNavigationObserver observer(web_contents);
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
observer.WaitForNavigationFinished();
EXPECT_EQ(extension->GetResourceURL(resource),
observer.last_navigation_url());
EXPECT_EQ(expect_net_error == net::OK,
observer.last_navigation_succeeded());
EXPECT_EQ(expect_net_error, observer.last_net_error_code());
histogram_tester.ExpectBucketCount(kHistogramName, is_accessible, 1);
};
// Test cases.
server_redirect(net::OK, "web_accessible_resource.html", true);
server_redirect(net::ERR_BLOCKED_BY_CLIENT, "resource.html", false);
};
auto TestBrowserRedirectMV2 = [&]() {
TestBrowserRedirect(
R"({
"name": "Test browser redirect",
"version": "0.1",
"manifest_version": 2,
"web_accessible_resources": ["web_accessible_resource.html"]
})",
"Extensions.WAR.XOriginWebAccessible.MV2");
};
auto TestBrowserRedirectMV3 = [&]() {
TestBrowserRedirect(
R"({
"name": "Redirect Test",
"version": "0.1",
"manifest_version": 3,
"web_accessible_resources": [
{
"resources": ["web_accessible_resource.html"],
"matches": ["http://example.com/*"]
}
]
})",
"Extensions.WAR.XOriginWebAccessible.MV3");
};
TestBrowserRedirectMV2();
TestBrowserRedirectMV3();
}
// Verify browser process redirect to an non web accessible resource. Navigate
// to a webpage that's redirected by DNR to a web server that initiates a
// redirect to a non web accessible extension resource.
IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserTest,
BrowserProcessRedirectMainframeReachability) {
auto TestBrowserRedirectImpl = [&](const std::string& manifest) {
// Load extension.
TestExtensionDir test_dir;
test_dir.WriteManifest(manifest);
test_dir.WriteFile(FILE_PATH_LITERAL("inaccessible.html"),
"inaccessible.html");
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
base::StringPrintf(
R"(
// Promisable function for getDynamicRules(), which is unavailable
// before MV3. Returns a promise that can be resolved or rejected.
async function getDynamicRules() {
return new Promise(async (resolve, reject) => {
chrome.declarativeNetRequest.getDynamicRules(rules => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
return resolve(rules);
});
});
}
// Ensure that the expected rule has loaded before continuing.
async function waitForRuleId(ruleId) {
const start = Date.now();
const timeout = 3000;
const sleep = 100;
let rules;
while (Date.now() - start < timeout) {
try {
rules = await getDynamicRules();
} catch(error) {
// Try to get rules again, until either success or timeout.
continue;
}
if (rules.some(rule => rule.id === ruleId)) {
// Exit this function now that the rule has been awaited.
return true;
}
// Sleep for a bit before trying again to match the rule.
await new Promise(resolve => setTimeout(resolve, sleep));
continue;
}
// A matching rule id wasn't found.
return false;
}
chrome.runtime.onInstalled.addListener(async () => {
const ruleId = 1;
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
"id": ruleId,
"action": {
"type": "redirect",
"redirect": {
"url":
`%s?${chrome.runtime.getURL('inaccessible.html')}`
}
},
"condition": {
"urlFilter": "example.com*/empty.html",
"resourceTypes": ["main_frame"]
}
}]
});
chrome.test.assertTrue(await waitForRuleId(ruleId));
chrome.test.notifyPass();
});
)",
embedded_test_server()
->GetURL("b.example.com", "/server-redirect")
.spec()));
extensions::ResultCatcher catcher;
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(catcher.GetNextResult());
// Navigate to a webpage that eventually navigates to an extension resource.
int expect_net_error = net::ERR_BLOCKED_BY_CLIENT;
const char* resource = "inaccessible.html";
GURL gurl = embedded_test_server()->GetURL("a.example.com", "/empty.html");
auto* web_contents = GetActiveWebContents();
content::TestNavigationObserver observer(web_contents);
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), gurl));
observer.WaitForNavigationFinished();
EXPECT_EQ(expect_net_error == net::OK,
observer.last_navigation_succeeded());
EXPECT_EQ(expect_net_error, observer.last_net_error_code());
EXPECT_EQ(extension->GetResourceURL(resource),
observer.last_navigation_url());
};
using ManifestVersion = enum { MV3, MV2 };
auto TestBrowserRedirect = [&TestBrowserRedirectImpl](
ManifestVersion manifest_version) {
std::string manifest_base = base::StringPrintf(
R"(
"name": "test",
"version": "1",
"manifest_version": %d
)",
manifest_version == MV3 ? 3 : 2);
std::string manifest;
switch (manifest_version) {
case MV3:
manifest =
R"(
"background": {"service_worker": "background.js"},
"permissions": [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess"
],
"host_permissions": [
"<all_urls>"
]
)";
break;
case MV2:
manifest =
R"(
"background": {"scripts": ["background.js"]},
"permissions": [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"<all_urls>"
]
)";
break;
}
manifest = base::StringPrintf("{%s, %s}", manifest_base, manifest);
TestBrowserRedirectImpl(manifest);
};
TestBrowserRedirect(MV3);
TestBrowserRedirect(MV2);
}
// Test dynamic origins in web accessible resources.
// TODO(crbug.com/352267920): Move to web_accessible_resources_browsertest.cc?
class DynamicOriginBrowserTest : public ExtensionBrowserTest {
public:
DynamicOriginBrowserTest() = default;
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
InstallExtension();
}
protected:
const Extension* GetExtension() { return extension_; }
content::RenderFrameHost* GetPrimaryMainFrame() {
return GetActiveWebContents()->GetPrimaryMainFrame();
}
private:
void InstallExtension() {
dir_.WriteManifest(R"({
"name": "Extension",
"version": "1.0",
"manifest_version": 3,
"web_accessible_resources": [{
"resources": ["web_accessible_resource.html", "ok.html"],
"matches": ["<all_urls>"]
}]
})");
std::vector<std::string> files(
{"extension_resource.html", "web_accessible_resource.html", "ok.html"});
for (const auto& filename : files) {
dir_.WriteFile(base::FilePath::FromASCII(filename).value(), filename);
}
extension_ = LoadExtension(dir_.UnpackedPath());
DCHECK(extension_);
}
raw_ptr<const Extension, DanglingUntriaged> extension_ = nullptr;
TestExtensionDir dir_;
};
// Test a dynamic url as a web accessible resource.
IN_PROC_BROWSER_TEST_F(DynamicOriginBrowserTest, DynamicUrl) {
auto* extension = GetExtension();
// Resource and extension origin should match.
{
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), extension->GetResourceURL("ok.html")));
ASSERT_EQ(extension->origin(),
GetPrimaryMainFrame()->GetLastCommittedOrigin());
}
// Dynamic resource should resolve to static url.
{
GURL static_url = extension->url().Resolve("ok.html");
GURL dynamic_url = extension->dynamic_url().Resolve("ok.html");
ASSERT_NE(static_url, dynamic_url);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), dynamic_url));
EXPECT_EQ(static_url, GetPrimaryMainFrame()->GetLastCommittedURL());
EXPECT_EQ(extension->origin(),
GetPrimaryMainFrame()->GetLastCommittedOrigin());
}
}
// Error accessing resource from random guid.
IN_PROC_BROWSER_TEST_F(DynamicOriginBrowserTest,
InvalidDynamicResourceFailsToLoad) {
auto* extension = GetExtension();
auto run = [&](const GURL& gurl, int status) {
content::WebContents* web_contents = GetActiveWebContents();
content::TestNavigationObserver nav_observer(web_contents);
web_contents->GetController().LoadURL(
gurl, content::Referrer(), ui::PageTransition::PAGE_TRANSITION_TYPED,
std::string());
nav_observer.Wait();
EXPECT_EQ(status == net::OK, nav_observer.last_navigation_succeeded());
EXPECT_EQ(status, nav_observer.last_net_error_code());
};
auto random_guid = base::Uuid::GenerateRandomV4().AsLowercaseString();
GURL random_url =
Extension::GetBaseURLFromExtensionId(random_guid).Resolve("ok.html");
GURL dynamic_url = extension->dynamic_url().Resolve("ok.html");
run(random_url, net::ERR_BLOCKED_BY_CLIENT);
run(dynamic_url, net::OK);
}
// Web Accessible Resources.
IN_PROC_BROWSER_TEST_F(DynamicOriginBrowserTest, FetchGuidFromFrame) {
auto* extension = GetExtension();
// Fetch url from frame to verify with expectations.
auto test_frame_with_fetch = [&](const GURL& frame_url,
const GURL& expected_frame_url,
const GURL& fetch_url,
const char* expected_fetch_url_contents) {
SCOPED_TRACE(testing::Message() << "test_frame_with_fetch"
<< ": frame_url = " << frame_url
<< "; fetch_url = " << fetch_url);
// Fetch and test resource.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), frame_url));
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_EQ(expected_frame_url,
web_contents->GetPrimaryMainFrame()->GetLastCommittedURL());
constexpr char kFetchScriptTemplate[] =
R"(
fetch($1).then(result => {
return result.text();
}).catch(err => {
return String(err);
});)";
EXPECT_EQ(
expected_fetch_url_contents,
content::EvalJs(web_contents, content::JsReplace(kFetchScriptTemplate,
fetch_url.spec())));
};
const struct {
const char* title;
GURL frame_url;
GURL expected_frame_url;
GURL fetch_url;
const char* expected_fetch_url_contents;
} test_cases[] = {
{
"Fetch web accessible resource from extension resource.",
extension->url().Resolve("extension_resource.html"),
extension->url().Resolve("extension_resource.html"),
extension->url().Resolve("web_accessible_resource.html"),
"web_accessible_resource.html",
},
{
"Fetch dynamic web accessible resource from extension resource.",
extension->url().Resolve("extension_resource.html"),
extension->url().Resolve("extension_resource.html"),
extension->dynamic_url().Resolve("web_accessible_resource.html"),
"web_accessible_resource.html",
},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message() << test_case.title);
test_frame_with_fetch(test_case.frame_url, test_case.expected_frame_url,
test_case.fetch_url,
test_case.expected_fetch_url_contents);
}
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
} // namespace extensions