blob: 1727b96486544691f1e070e48620bc1eebe58ab1 [file]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include <string_view>
#include <utility>
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_agent_host_client.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/network_service_util.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/result_codes.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "services/network/public/mojom/network_service_test.mojom.h"
#include "ui/base/page_transition_types.h"
#include "url/url_constants.h"
namespace content {
class RenderFrameDevToolsAgentHostBrowserTest : public ContentBrowserTest {
public:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
}
};
namespace {
// A DevToolsAgentHostClient implementation doing nothing.
class StubDevToolsAgentHostClient : public content::DevToolsAgentHostClient {
public:
StubDevToolsAgentHostClient() {}
~StubDevToolsAgentHostClient() override {}
void AgentHostClosed(content::DevToolsAgentHost* agent_host) override {}
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
base::span<const uint8_t> message) override {}
bool MayAttachToURL(const GURL& url, bool is_webui) override {
return true;
}
};
} // namespace
IN_PROC_BROWSER_TEST_F(RenderFrameDevToolsAgentHostBrowserTest,
OlderCommitKeepsPendingNavigationTarget) {
ASSERT_TRUE(embedded_test_server()->Start());
Shell* new_shell =
Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
GURL(), nullptr, gfx::Size());
auto* web_contents = static_cast<WebContentsImpl*>(new_shell->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
RenderFrameHostImpl* frame_a = web_contents->GetPrimaryMainFrame();
scoped_refptr<DevToolsAgentHost> agent_host =
DevToolsAgentHost::GetOrCreateFor(web_contents);
auto* frame_agent =
static_cast<RenderFrameDevToolsAgentHost*>(agent_host.get());
// Hold the initial about:blank commit in the current frame while a newer
// navigation becomes ready to commit in a different frame.
CommitNavigationPauser initial_commit(frame_a);
NavigationController::LoadURLParams initial_params{GURL(url::kAboutBlankURL)};
initial_params.transition_type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
auto initial_navigation =
web_contents->GetController().LoadURLWithParams(initial_params);
initial_commit.WaitForCommitAndPause();
ASSERT_TRUE(initial_navigation);
ASSERT_TRUE(initial_navigation->IsWaitingToCommit());
ASSERT_EQ(frame_a, initial_navigation->GetRenderFrameHost());
TestDevToolsProtocolClient client;
client.AttachToWebContents(web_contents);
base::ScopedClosureRunner detach_client(
base::BindLambdaForTesting([&] { client.DetachProtocolClient(); }));
ASSERT_TRUE(agent_host->IsAttached());
const GURL destination =
embedded_test_server()->GetURL("next.test", "/title1.html");
TestNavigationManager newer_navigation(web_contents, destination);
new_shell->LoadURL(destination);
ASSERT_TRUE(newer_navigation.WaitForResponse());
RenderFrameHostImpl* frame_b =
root->render_manager()->speculative_frame_host();
ASSERT_TRUE(frame_b);
ASSERT_NE(frame_a, frame_b);
CommitNavigationPauser newer_commit(frame_b);
newer_navigation.ResumeNavigation();
newer_commit.WaitForCommitAndPause();
ASSERT_TRUE(newer_navigation.GetNavigationHandle()->IsWaitingToCommit());
ASSERT_EQ(frame_b, frame_agent->GetFrameHostForTesting());
initial_commit.ResumePausedCommit();
EXPECT_FALSE(initial_navigation);
EXPECT_EQ(GURL(url::kAboutBlankURL), web_contents->GetLastCommittedURL());
EXPECT_EQ(frame_a, web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(newer_navigation.GetNavigationHandle()->IsWaitingToCommit());
EXPECT_EQ(frame_b, frame_agent->GetFrameHostForTesting());
newer_commit.ResumePausedCommit();
ASSERT_TRUE(newer_navigation.WaitForNavigationFinished());
ASSERT_TRUE(WaitForLoadStop(web_contents));
EXPECT_EQ(frame_b, web_contents->GetPrimaryMainFrame());
EXPECT_EQ(frame_b, frame_agent->GetFrameHostForTesting());
EXPECT_FALSE(web_contents->IsCrashed());
base::DictValue params;
params.Set("expression", "location.href");
params.Set("returnByValue", true);
const base::DictValue* result =
client.SendCommandSync("Runtime.evaluate", std::move(params));
ASSERT_TRUE(result);
const std::string* value = result->FindStringByDottedPath("result.value");
ASSERT_TRUE(value);
EXPECT_EQ(destination.spec(), *value);
}
// This test checks which RenderFrameHostImpl the RenderFrameDevToolsAgentHost
// is tracking while a cross-site navigation is canceled after having reached
// the ReadyToCommit stage.
// See https://crbug.com/695203.
// TODO(crbug.com/40916125): Re-enable this test
#define MAYBE_CancelCrossOriginNavigationAfterReadyToCommit \
DISABLED_CancelCrossOriginNavigationAfterReadyToCommit
IN_PROC_BROWSER_TEST_F(RenderFrameDevToolsAgentHostBrowserTest,
MAYBE_CancelCrossOriginNavigationAfterReadyToCommit) {
net::test_server::ControllableHttpResponse response_b(embedded_test_server(),
"/response_b");
net::test_server::ControllableHttpResponse response_c(embedded_test_server(),
"/response_c");
EXPECT_TRUE(embedded_test_server()->Start());
// 1) Loads a document.
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
WebContentsImpl* web_contents_impl =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents_impl->GetPrimaryFrameTree().root();
// 2) Open DevTools.
scoped_refptr<DevToolsAgentHost> devtools_agent =
DevToolsAgentHost::GetOrCreateFor(web_contents_impl);
RenderFrameDevToolsAgentHost* rfh_devtools_agent =
static_cast<RenderFrameDevToolsAgentHost*>(devtools_agent.get());
// 3) Tries to navigate cross-site, but do not commit the navigation.
// Send only the headers such that it reaches the ReadyToCommit stage, but
// not further.
// 3.a) Navigation: Start.
GURL url_b(embedded_test_server()->GetURL("b.com", "/response_b"));
TestNavigationManager observer_b(shell()->web_contents(), url_b);
shell()->LoadURL(url_b);
observer_b.WaitForSpeculativeRenderFrameHostCreation();
RenderFrameHostImpl* current_rfh =
root->render_manager()->current_frame_host();
RenderFrameHostImpl* speculative_rfh_b =
root->render_manager()->speculative_frame_host();
EXPECT_TRUE(current_rfh);
EXPECT_TRUE(speculative_rfh_b);
EXPECT_EQ(current_rfh, rfh_devtools_agent->GetFrameHostForTesting());
// 3.b) Navigation: ReadyToCommit.
response_b.WaitForRequest();
response_b.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
EXPECT_TRUE(observer_b.WaitForResponse()); // Headers are received.
observer_b.ResumeNavigation(); // ReadyToCommitNavigation is called.
EXPECT_EQ(speculative_rfh_b, rfh_devtools_agent->GetFrameHostForTesting());
if (!AreAllSitesIsolatedForTesting()) {
EXPECT_TRUE(speculative_rfh_b->GetSiteInstance()->IsDefaultSiteInstance());
}
// 4) Navigate elsewhere, it will cancel the previous navigation if navigation
// queueing is not enabled.
// 4.a) Navigation: Start.
GURL url_c(embedded_test_server()->GetURL("c.com", "/response_c"));
TestNavigationManager observer_c(shell()->web_contents(), url_c);
shell()->LoadURL(url_c);
EXPECT_TRUE(observer_c.WaitForRequestStart());
RenderFrameHostImpl* speculative_rfh_c = nullptr;
// With navigation queueing, starting a new navigation won't cancel an
// existing pending commit navigation, so wait for the first navigation to
// finish first before continuing.
EXPECT_EQ(speculative_rfh_b,
root->render_manager()->speculative_frame_host());
EXPECT_EQ(speculative_rfh_b, rfh_devtools_agent->GetFrameHostForTesting());
ASSERT_TRUE(observer_b.WaitForNavigationFinished());
// 4.b) Navigation: ReadyToCommit.
observer_c.ResumeNavigation(); // Send the request.
response_c.WaitForRequest();
response_c.Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"\r\n");
EXPECT_TRUE(observer_c.WaitForResponse()); // Headers are received.
observer_c.ResumeNavigation(); // ReadyToCommitNavigation is called.
speculative_rfh_c = root->render_manager()->speculative_frame_host();
EXPECT_EQ(speculative_rfh_c, rfh_devtools_agent->GetFrameHostForTesting());
// 4.c) Navigation: Commit.
response_c.Send("<html><body> response's body </body></html>");
response_c.Done();
ASSERT_TRUE(observer_c.WaitForNavigationFinished());
EXPECT_EQ(speculative_rfh_c, root->render_manager()->current_frame_host());
EXPECT_EQ(speculative_rfh_c, rfh_devtools_agent->GetFrameHostForTesting());
}
// Regression test for https://crbug.com/795694.
// * Open chrome://dino
// * Open DevTools
// * Reload from DevTools must work.
IN_PROC_BROWSER_TEST_F(RenderFrameDevToolsAgentHostBrowserTest,
ReloadDinoPage) {
// 1) Navigate to chrome://dino.
GURL dino_url(kChromeUIScheme + std::string("://") + kChromeUIDinoHost);
EXPECT_FALSE(NavigateToURL(shell(), dino_url));
// 2) Open DevTools.
scoped_refptr<DevToolsAgentHost> devtools_agent_host =
DevToolsAgentHost::GetOrCreateFor(shell()->web_contents());
StubDevToolsAgentHostClient devtools_agent_host_client;
devtools_agent_host->AttachClient(&devtools_agent_host_client);
// 3) Reload from DevTools.
TestNavigationObserver reload_observer(shell()->web_contents());
constexpr char kMsg[] = R"({"id":1,"method":"Page.reload"})";
devtools_agent_host->DispatchProtocolMessage(
&devtools_agent_host_client, base::byte_span_from_cstring(kMsg));
reload_observer.Wait();
devtools_agent_host->DetachClient(&devtools_agent_host_client);
}
IN_PROC_BROWSER_TEST_F(RenderFrameDevToolsAgentHostBrowserTest,
CheckDebuggerAttachedToTabTarget) {
EXPECT_TRUE(embedded_test_server()->Start());
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
WebContents* web_contents = shell()->web_contents();
scoped_refptr<DevToolsAgentHost> devtools_agent_host =
DevToolsAgentHost::GetOrCreateForTab(web_contents);
StubDevToolsAgentHostClient devtools_agent_host_client;
devtools_agent_host->AttachClient(&devtools_agent_host_client);
EXPECT_TRUE(DevToolsAgentHost::IsDebuggerAttached(web_contents));
devtools_agent_host->DetachClient(&devtools_agent_host_client);
}
class RenderFrameDevToolsAgentHostRawHeadersBrowserTest
: public RenderFrameDevToolsAgentHostBrowserTest {
public:
RenderFrameDevToolsAgentHostRawHeadersBrowserTest() {
#if BUILDFLAG(IS_ANDROID)
// Network service is in-process by default on Android. Force it
// out-of-process so we can use the BindTestInterfaceForTesting which
// requires a registry that is only present in the utility process or when
// forced OOP.
ForceOutOfProcessNetworkService();
#endif
}
};
IN_PROC_BROWSER_TEST_F(RenderFrameDevToolsAgentHostRawHeadersBrowserTest,
RawHeadersAccess) {
EXPECT_TRUE(embedded_test_server()->Start());
// 1) Loads a document.
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
WebContentsImpl* web_contents_impl =
static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHostImpl* rfh = web_contents_impl->GetPrimaryMainFrame();
uint32_t process_id = rfh->GetProcess()->GetID().GetUnsafeValue();
mojo::Remote<network::mojom::NetworkServiceTest>& network_service_test_remote =
network_service_test();
if (!network_service_test_remote.is_bound()) {
GetNetworkService()->BindTestInterfaceForTesting(
network_service_test_remote.BindNewPipeAndPassReceiver());
}
// 2) Verify raw headers access is NOT granted initially.
{
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
bool has_access = true;
network_service_test_remote->HasRawHeadersAccess(
process_id, url_a, base::BindLambdaForTesting([&](bool result) {
has_access = result;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_FALSE(has_access);
}
// 3) Attach DevTools.
StubDevToolsAgentHostClient client;
scoped_refptr<DevToolsAgentHost> devtools_agent =
DevToolsAgentHost::GetOrCreateFor(web_contents_impl);
devtools_agent->AttachClient(&client);
// 4) Verify raw headers access IS granted.
{
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
bool has_access = false;
network_service_test_remote->HasRawHeadersAccess(
process_id, url_a, base::BindLambdaForTesting([&](bool result) {
has_access = result;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(has_access);
}
// 5) Detach DevTools.
devtools_agent->DetachClient(&client);
// 6) Verify raw headers access is NOT granted.
{
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
bool has_access = true;
network_service_test_remote->HasRawHeadersAccess(
process_id, url_a, base::BindLambdaForTesting([&](bool result) {
has_access = result;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_FALSE(has_access);
}
}
} // namespace content