910e62b5创建于 1月15日历史提交
// Copyright 2021 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/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/devtools/protocol/devtools_protocol_test_support.h"
#include "content/browser/loader/navigation_early_hints_manager.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/embedded_test_server_connection_listener.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/quic_simple_test_server.h"
#include "net/test/test_data_directory.h"
#include "net/third_party/quiche/src/quiche/common/http/http_header_block.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/link_header.mojom.h"

namespace content {

using PreloadedResources = NavigationEarlyHintsManager::PreloadedResources;

namespace {

struct HeaderField {
  HeaderField(const std::string& name, const std::string& value)
      : name(name), value(value) {}

  std::string name;
  std::string value;
};

struct ResponseEntry {
  ResponseEntry(const std::string& path, net::HttpStatusCode status_code)
      : path(path) {
    headers[":path"] = path;
    headers[":status"] = base::StringPrintf("%d", status_code);
  }

  void AddEarlyHints(const std::vector<HeaderField>& header_fields) {
    quiche::HttpHeaderBlock hints_headers;
    for (const auto& header : header_fields)
      hints_headers.AppendValueOrAddHeader(header.name, header.value);
    early_hints.push_back(std::move(hints_headers));
  }

  std::string path;
  quiche::HttpHeaderBlock headers;
  std::string body;
  std::vector<quiche::HttpHeaderBlock> early_hints;
};

const char kPageWithHintedScriptPath[] = "/page_with_hinted_js.html";
const char kPageWithHintedScriptBody[] = "<script src=\"/hinted.js\"></script>";

const char kPageWithHintedCorsScriptPath[] = "/page_with_hinted_cors_js.html";
const char kPageWithHintedCorsScriptBody[] =
    "<script src=\"/hinted.js\" crossorigin></script>";

const char kPageWithIframePath[] = "/page_with_iframe.html";
const char kPageWithIframeBody[] =
    "<iframe src=\"page_with_hinted_js.html\"></iframe>";

const char kPageWithHintedModuleScriptPath[] =
    "/page_with_hinted_module_js.html";
const char kPageWithHintedModuleScriptBody[] =
    "<script src=\"/hinted.js\" type=\"module\"></script>";

const char kHintedScriptPath[] = "/hinted.js";
const char kHintedScriptBody[] = "document.title = 'Done';";

const char kHintedStylesheetPath[] = "/hinted.css";
const char kHintedStylesheetBody[] = "/*empty*/";

const char kEmptyPagePath[] = "/empty.html";
const char kEmptyPageBody[] = "<html></html>";

const char kRedirectedPagePath[] = "/redirected.html";
const char kRedirectedPageBody[] = "<script src=\"/hinted.js\"></script>";

// Listens to sockets on an EmbeddedTestServer for preconnect tests. Created
// on the UI thread. EmbeddedTestServerConnectionListener methods are called
// from a different thread than the UI thread.
class PreconnectListener
    : public net::test_server::EmbeddedTestServerConnectionListener {
 public:
  PreconnectListener()
      : task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
        weak_ptr_factory_(this) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  }
  ~PreconnectListener() override = default;

  // net::test_server::EmbeddedTestServerConnectionListener implementation:
  std::unique_ptr<net::StreamSocket> AcceptedSocket(
      std::unique_ptr<net::StreamSocket> connection) override {
    task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&PreconnectListener::AcceptedSocketOnUIThread,
                                  weak_ptr_factory_.GetWeakPtr()));
    return connection;
  }
  void ReadFromSocket(const net::StreamSocket& connection, int rv) override {}

  size_t num_accepted_sockets() {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    return num_accepted_sockets_;
  }

 private:
  void AcceptedSocketOnUIThread() {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    ++num_accepted_sockets_;
  }

  scoped_refptr<base::SequencedTaskRunner> task_runner_;
  size_t num_accepted_sockets_ = 0;

  base::WeakPtrFactory<PreconnectListener> weak_ptr_factory_;
};

}  // namespace

// Most tests use EmbeddedTestServer but this uses QuicSimpleTestServer because
// Early Hints are only plumbed over HTTP/2 or HTTP/3 (QUIC).
class NavigationEarlyHintsTest : public DevToolsProtocolTest {
 public:
  NavigationEarlyHintsTest() {
    feature_list_.InitWithFeatures(
        std::vector<base::test::FeatureRef>{
            net::features::kSplitCacheByNetworkIsolationKey},
        std::vector<base::test::FeatureRef>{
            net::features::kMigrateSessionsOnNetworkChangeV2});
  }
  ~NavigationEarlyHintsTest() override = default;

  void SetUpOnMainThread() override {
    DevToolsProtocolTest::SetUpOnMainThread();
    ConfigureMockCertVerifier();
    host_resolver()->AddRule("*", "127.0.0.1");

    cross_origin_server_.RegisterRequestHandler(
        base::BindRepeating(&NavigationEarlyHintsTest::HandleCrossOriginRequest,
                            base::Unretained(this)));
    preconnect_listener_ = std::make_unique<PreconnectListener>();
    cross_origin_server().SetConnectionListener(preconnect_listener_.get());
    ASSERT_TRUE(cross_origin_server_.Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII(switches::kOriginToForceQuicOn, "*");
    mock_cert_verifier_.SetUpCommandLine(command_line);

    ASSERT_TRUE(net::QuicSimpleTestServer::Start());

    DevToolsProtocolTest::SetUpCommandLine(command_line);
  }

  void TearDown() override {
    base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait;
    net::QuicSimpleTestServer::Shutdown();
    DevToolsProtocolTest::TearDown();
  }

  net::test_server::EmbeddedTestServer& cross_origin_server() {
    return cross_origin_server_;
  }

  std::string WaitForHintedScriptDevtoolsRequestId() {
    base::Value::Dict result;
    while (true) {
      result = WaitForNotification("Network.requestWillBeSent", true);
      const base::Value* request_url = result.FindByDottedPath("request.url");
      if (request_url->GetString() ==
          net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath).spec()) {
        return *result.FindString("requestId");
      }
    }
  }

  base::Value::Dict WaitForDevtoolsEarlyHints() {
    base::Value::Dict result;
    while (true) {
      result = WaitForNotification("Network.responseReceivedEarlyHints", true);
      return result;
    }
  }

  base::Value::Dict WaitForResponseReceived(const std::string& request_id) {
    base::Value::Dict result;
    while (true) {
      result = WaitForNotification("Network.responseReceived", true);
      const std::string* received_id = result.FindString("requestId");
      if (received_id && *received_id == request_id) {
        return result;
      }
    }
  }

 protected:
  base::test::ScopedFeatureList& feature_list() { return feature_list_; }

  PreconnectListener& preconnect_listener() { return *preconnect_listener_; }

  void SetUpInProcessBrowserTestFixture() override {
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  void ConfigureMockCertVerifier() {
    auto test_cert =
        net::ImportCertFromFile(net::GetTestCertsDirectory(), "quic-chain.pem");
    net::CertVerifyResult verify_result;
    verify_result.verified_cert = test_cert;
    mock_cert_verifier_.mock_cert_verifier()->AddResultForCert(
        test_cert, verify_result, net::OK);
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
  }

  HeaderField CreatePreloadLinkForScript() {
    return HeaderField(
        "link",
        base::StringPrintf("<%s>; rel=preload; as=script", kHintedScriptPath));
  }

  HeaderField CreatePreloadLinkForCorsScript() {
    return HeaderField(
        "link", base::StringPrintf("<%s>; rel=preload; as=script; crossorigin",
                                   kHintedScriptPath));
  }

  HeaderField CreateModulePreloadLink() {
    return HeaderField("link", base::StringPrintf("<%s>; rel=modulepreload",
                                                  kHintedScriptPath));
  }

  HeaderField CreatePreloadLinkForStylesheet() {
    return HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=style",
                                                  kHintedStylesheetPath));
  }

  void RegisterResponse(const ResponseEntry& entry) {
    net::QuicSimpleTestServer::AddResponseWithEarlyHints(
        entry.path, entry.headers, entry.body, entry.early_hints);
  }

  void RegisterHintedScriptResource() {
    ResponseEntry hinted_script_entry(kHintedScriptPath, net::HTTP_OK);
    hinted_script_entry.headers["content-type"] = "application/javascript";
    hinted_script_entry.headers["cache-control"] = "max-age=3600";
    hinted_script_entry.body = kHintedScriptBody;
    RegisterResponse(hinted_script_entry);
  }

  void RegisterHintedStylesheetResource() {
    ResponseEntry hinted_script_entry(kHintedStylesheetPath, net::HTTP_OK);
    hinted_script_entry.headers["content-type"] = "text/css";
    hinted_script_entry.body = kHintedStylesheetBody;
    RegisterResponse(hinted_script_entry);
  }

  void RegisterRedirectedPage() {
    ResponseEntry entry(kRedirectedPagePath, net::HTTP_OK);
    entry.body = kRedirectedPageBody;
    RegisterResponse(entry);
  }

  ResponseEntry CreatePageEntryWithHintedScript(
      net::HttpStatusCode status_code) {
    RegisterHintedScriptResource();

    ResponseEntry entry(kPageWithHintedScriptPath, status_code);
    entry.body = kPageWithHintedScriptBody;
    HeaderField link_header = CreatePreloadLinkForScript();
    entry.AddEarlyHints({std::move(link_header)});

    return entry;
  }

  ResponseEntry CreateEmptyPageEntryWithHintedScript() {
    RegisterHintedScriptResource();

    ResponseEntry entry(kEmptyPagePath, net::HTTP_OK);
    entry.body = kEmptyPageBody;
    HeaderField link_header = CreatePreloadLinkForScript();
    entry.AddEarlyHints({std::move(link_header)});

    return entry;
  }

  ResponseEntry CreatePageEntryWithHintedCorsScript(
      net::HttpStatusCode status_code) {
    RegisterHintedScriptResource();

    ResponseEntry entry(kPageWithHintedCorsScriptPath, status_code);
    entry.body = kPageWithHintedCorsScriptBody;
    HeaderField link_header = CreatePreloadLinkForCorsScript();
    entry.AddEarlyHints({std::move(link_header)});

    return entry;
  }

  ResponseEntry CreatePageEntryWithHintedModuleScript(
      net::HttpStatusCode status_code) {
    RegisterHintedScriptResource();

    ResponseEntry entry(kPageWithHintedModuleScriptPath, status_code);
    entry.body = kPageWithHintedModuleScriptBody;
    HeaderField link_header = CreateModulePreloadLink();
    entry.AddEarlyHints({std::move(link_header)});

    return entry;
  }

  bool NavigateToURLAndWaitTitle(const GURL& url, const std::string& title) {
    return NavigateToURLAndWaitTitleWithCommitURL(url, url, title);
  }

  bool NavigateToURLAndWaitTitleWithCommitURL(const GURL& url,
                                              const GURL& expected_commit_url,
                                              const std::string& title) {
    std::u16string title16 = base::ASCIIToUTF16(title);
    TitleWatcher title_watcher(shell()->web_contents(), title16);
    if (!NavigateToURL(shell(), url, expected_commit_url)) {
      return false;
    }
    return title16 == title_watcher.WaitAndGetTitle();
  }

  NavigationEarlyHintsManager* GetEarlyHintsManager(RenderFrameHostImpl* rfh) {
    return rfh->early_hints_manager();
  }

  PreloadedResources WaitForPreloadedResources() {
    return WaitForPreloadedResources(static_cast<RenderFrameHostImpl*>(
        shell()->web_contents()->GetPrimaryMainFrame()));
  }

  PreloadedResources WaitForPreloadedResources(RenderFrameHostImpl* rfh) {
    base::RunLoop loop;
    PreloadedResources result;
    if (!GetEarlyHintsManager(rfh)) {
      return result;
    }

    GetEarlyHintsManager(rfh)->WaitForPreloadsFinishedForTesting(
        base::BindLambdaForTesting([&](PreloadedResources preloaded_resources) {
          result = preloaded_resources;
          loop.Quit();
        }));
    loop.Run();
    return result;
  }

  enum class FetchResult {
    kFetched,
    kBlocked,
  };
  FetchResult FetchScriptOnDocument(ToRenderFrameHost target, GURL src) {
    EvalJsResult result = EvalJs(target, JsReplace(R"(
      new Promise(resolve => {
        const script = document.createElement("script");
        script.src = $1;
        script.onerror = () => resolve("blocked");
        script.onload = () => resolve("fetched");
        document.body.appendChild(script);
      });
    )",
                                                   src));
    return result.ExtractString() == "fetched" ? FetchResult::kFetched
                                               : FetchResult::kBlocked;
  }

 private:
  std::unique_ptr<net::test_server::HttpResponse> HandleCrossOriginRequest(
      const net::test_server::HttpRequest& request) {
    GURL relative_url = request.base_url.Resolve(request.relative_url);

    if (relative_url.GetPath() == kEmptyPagePath) {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_OK);
      response->set_content_type("text/html");
      response->set_content("");
      return std::move(response);
    }

    if (relative_url.GetPath() == kRedirectedPagePath) {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_OK);
      response->set_content_type("text/html");
      response->set_content(kRedirectedPageBody);
      return std::move(response);
    }

    if (relative_url.GetPath() != kHintedScriptPath) {
      return nullptr;
    }

    auto response = std::make_unique<net::test_server::BasicHttpResponse>();
    response->set_code(net::HTTP_OK);
    response->set_content_type("application/javascript");
    response->set_content(kHintedScriptBody);

    std::string query = relative_url.GetQuery();
    if (query == "corp-cross-origin") {
      response->AddCustomHeader("Cross-Origin-Resource-Policy", "cross-origin");
    } else if (query == "corp-same-origin") {
      response->AddCustomHeader("Cross-Origin-Resource-Policy", "same-origin");
    }

    return std::move(response);
  }

  base::test::ScopedFeatureList feature_list_;

  ContentMockCertVerifier mock_cert_verifier_;

  // For tests that fetch resources from a cross origin server.
  net::EmbeddedTestServer cross_origin_server_;
  std::unique_ptr<PreconnectListener> preconnect_listener_;
};

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, Basic) {
  base::HistogramTester histograms;

  ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitle(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
      "Done"));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  auto it = preloads.find(preloaded_url);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);

  histograms.ExpectTotalCount(
      "Navigation.EarlyHints.WillStartRequestToEarlyHintsTime", 1);
  histograms.ExpectTotalCount(
      "Navigation.EarlyHints.EarlyHintsToResponseStartTime", 1);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CorsAttribute) {
  ResponseEntry entry = CreatePageEntryWithHintedCorsScript(net::HTTP_OK);
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitle(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedCorsScriptPath),
      "Done"));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  auto it = preloads.find(preloaded_url);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, ModulePreload) {
  ResponseEntry entry = CreatePageEntryWithHintedModuleScript(net::HTTP_OK);
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitle(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedModuleScriptPath),
      "Done"));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  auto it = preloads.find(preloaded_url);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DisallowPreloadFromIframe) {
  ResponseEntry page_entry(kPageWithIframePath, net::HTTP_OK);
  page_entry.body = kPageWithIframeBody;
  RegisterResponse(page_entry);

  ResponseEntry iframe_entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
  RegisterResponse(iframe_entry);

  EXPECT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithIframePath)));

  // Find RenderFrameHost for the iframe.
  std::vector<RenderFrameHost*> all_frames =
      CollectAllRenderFrameHosts(shell()->web_contents());
  ASSERT_EQ(all_frames.size(), 2UL);
  ASSERT_EQ(all_frames[0], all_frames[1]->GetParent());
  RenderFrameHostImpl* iframe_host =
      static_cast<RenderFrameHostImpl*>(all_frames[1]);

  EXPECT_TRUE(WaitForLoadStop(WebContents::FromRenderFrameHost(iframe_host)));
  ASSERT_EQ(iframe_host->GetLastCommittedURL(),
            net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath));

  // NavigationEarlyHintsManager should not be created for subframes. If it were
  // created it should have been created before navigation commit.
  EXPECT_EQ(iframe_host->early_hints_manager(), nullptr);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NavigationServerError) {
  ResponseEntry entry =
      CreatePageEntryWithHintedScript(net::HTTP_INTERNAL_SERVER_ERROR);
  entry.body = "Internal Server Error";
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
                                         kPageWithHintedScriptPath)));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  auto it = preloads.find(preloaded_url);
  ASSERT_NE(it, preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectSameOrigin) {
  RegisterRedirectedPage();

  ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND);
  entry.headers["location"] = kRedirectedPagePath;
  entry.body = "";
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
      net::QuicSimpleTestServer::GetFileURL(kRedirectedPagePath), "Done"));

  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  auto it = preloads.find(preloaded_url);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectCrossOrigin) {
  const GURL kRedirectedUrl = cross_origin_server().GetURL(kRedirectedPagePath);

  ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND);
  entry.headers["location"] = kRedirectedUrl.spec();
  entry.body = "";
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
      kRedirectedUrl, "Done"));

  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_TRUE(preloads.empty());
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidPreloadLink) {
  const std::string kPath = "/hinted.html";

  RegisterHintedScriptResource();

  ResponseEntry entry(kPath, net::HTTP_OK);
  entry.body = "body";
  entry.AddEarlyHints(
      {HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=invalid",
                                              kHintedScriptPath))});
  RegisterResponse(entry);

  EXPECT_TRUE(
      NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath)));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_TRUE(preloads.empty());
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, MultipleEarlyHints) {
  RegisterHintedScriptResource();
  RegisterHintedStylesheetResource();

  ResponseEntry entry(kPageWithHintedScriptPath, net::HTTP_OK);
  entry.body = kPageWithHintedScriptBody;

  // Set two Early Hints responses which contain duplicate preload link headers.
  // The second response should be ignored.
  HeaderField script_link_header = CreatePreloadLinkForScript();
  HeaderField stylesheet_link_header = CreatePreloadLinkForStylesheet();
  entry.AddEarlyHints({script_link_header});
  entry.AddEarlyHints({script_link_header, stylesheet_link_header});
  RegisterResponse(entry);

  EXPECT_TRUE(NavigateToURLAndWaitTitle(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
      "Done"));
  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  GURL stylesheet_url =
      net::QuicSimpleTestServer::GetFileURL(kHintedStylesheetPath);
  EXPECT_TRUE(preloads.contains(script_url));
  EXPECT_FALSE(preloads.contains(stylesheet_url));
}

const char kPageWithCrossOriginScriptPage[] =
    "/page_with_cross_origin_script.html";

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Pass) {
  // The server response's is a script with
  // `Cross-Origin-Resource-Policy: cross-origin`.
  const GURL kCrossOriginScriptUrl =
      cross_origin_server().GetURL("/hinted.js?corp-cross-origin");

  ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
  HeaderField link_header = HeaderField(
      "link", base::StringPrintf("<%s>; rel=preload; as=script",
                                 kCrossOriginScriptUrl.spec().c_str()));
  page_entry.AddEarlyHints({std::move(link_header)});
  RegisterResponse(page_entry);

  EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
                                         kPageWithCrossOriginScriptPage)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
            FetchResult::kFetched);

  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  auto it = preloads.find(kCrossOriginScriptUrl);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(), net::OK);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Blocked) {
  // The server response's is a script with
  // `Cross-Origin-Resource-Policy: same-origin`.
  const GURL kCrossOriginScriptUrl =
      cross_origin_server().GetURL("/hinted.js?corp-same-origin");

  ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
  HeaderField link_header = HeaderField(
      "link", base::StringPrintf("<%s>; rel=preload; as=script",
                                 kCrossOriginScriptUrl.spec().c_str()));
  page_entry.AddEarlyHints({std::move(link_header)});
  RegisterResponse(page_entry);

  EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
                                         kPageWithCrossOriginScriptPage)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // The script fetch should be blocked.
  EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
            FetchResult::kBlocked);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Pass) {
  // The server sends `Cross-Origin-Resource-Policy: cross-origin`.
  const GURL kCrossOriginScriptUrl =
      cross_origin_server().GetURL("/hinted.js?corp-cross-origin");
  ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
  page_entry.headers["cross-origin-embedder-policy"] = "require-corp";
  HeaderField link_header = HeaderField(
      "link", base::StringPrintf("<%s>; rel=preload; as=script",
                                 kCrossOriginScriptUrl.spec().c_str()));
  HeaderField coep =
      HeaderField("cross-origin-embedder-policy", "require-corp");
  page_entry.AddEarlyHints({std::move(link_header), std::move(coep)});
  RegisterResponse(page_entry);

  EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
                                         kPageWithCrossOriginScriptPage)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
            FetchResult::kFetched);
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Block) {
  // The server does not send `Cross-Origin-Resource-Policy` header.
  const GURL kCrossOriginScriptUrl = cross_origin_server().GetURL("/hinted.js");
  ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
  page_entry.headers["cross-origin-embedder-policy"] = "require-corp";
  HeaderField link_header = HeaderField(
      "link", base::StringPrintf("<%s>; rel=preload; as=script",
                                 kCrossOriginScriptUrl.spec().c_str()));
  HeaderField coep =
      HeaderField("cross-origin-embedder-policy", "require-corp");
  page_entry.AddEarlyHints({std::move(link_header), std::move(coep)});
  RegisterResponse(page_entry);

  EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
                                         kPageWithCrossOriginScriptPage)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
            FetchResult::kBlocked);
}

// Test that network isolation key is set correctly for Early Hints preload.
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NetworkAnonymizationKey) {
  const GURL kHintedScriptUrl =
      net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);

  ResponseEntry entry = CreateEmptyPageEntryWithHintedScript();
  RegisterResponse(entry);

  std::optional<bool> is_cached;
  URLLoaderInterceptor interceptor(
      base::BindLambdaForTesting(
          [&](URLLoaderInterceptor::RequestParams* params) { return false; }),
      base::BindLambdaForTesting(
          [&](const GURL& request_url,
              const network::URLLoaderCompletionStatus& status) {
            if (request_url != kHintedScriptUrl) {
              return;
            }
            is_cached = status.exists_in_cache;
          }),
      base::NullCallback());

  ASSERT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath)));

  // Make sure the hinted resource is preloaded.
  PreloadedResources preloads = WaitForPreloadedResources();
  auto it = preloads.find(kHintedScriptUrl);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_EQ(it->second.error_code.value(), net::OK);

  ASSERT_FALSE(is_cached.value());
  is_cached = std::nullopt;

  // Fetch the hinted resource from the main frame. It should come from the
  // cache.
  FetchScriptOnDocument(shell(), kHintedScriptUrl);
  ASSERT_TRUE(is_cached.value());

  // Reset `is_cached` to make sure it is set true or false.
  is_cached = std::nullopt;

  // Create an iframe with a different origin and fetch the hinted resource from
  // the iframe. It should not come from the cache.
  auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHost* iframe =
      CreateSubframe(web_contents, /*frame_id=*/"",
                     cross_origin_server().GetURL("/empty.html"),
                     /*wait_for_navigation=*/true);
  FetchScriptOnDocument(iframe, kHintedScriptUrl);
  ASSERT_FALSE(is_cached.value());
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, SimplePreconnect) {
  const char kPageWithPreconnect[] = "/page_with_preconnect.html";
  const GURL kPreconnectUrl = cross_origin_server().GetURL("/");
  ResponseEntry page_entry(kPageWithPreconnect, net::HTTP_OK);
  HeaderField link_header =
      HeaderField("link", base::StringPrintf("<%s>; rel=preconnect",
                                             kPreconnectUrl.spec().c_str()));
  page_entry.AddEarlyHints({std::move(link_header)});
  RegisterResponse(page_entry);

  ASSERT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithPreconnect)));
  ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(preconnect_listener().num_accepted_sockets(), 1UL);
  EXPECT_TRUE(
      GetEarlyHintsManager(static_cast<RenderFrameHostImpl*>(
                               shell()->web_contents()->GetPrimaryMainFrame()))
          ->WasResourceHintsReceived());
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidHeader_NewLine) {
  const std::string kPath = "/header-contains-newline.html";
  ResponseEntry entry(kPath, net::HTTP_OK);
  entry.AddEarlyHints({HeaderField("invalid-header", "foo\r\nbar")});
  RegisterResponse(entry);
  EXPECT_FALSE(
      NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath)));
}

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DevtoolsEventsForEarlyHint) {
  ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
  RegisterResponse(entry);
  shell()->LoadURL(GURL("about:blank"));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  Attach();
  // Send this synchronously, otherwise it might not be sent yet when the
  // navigation start and the message sending gets suspended.
  SendCommandSync("Network.enable");
  GURL target_url =
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath);
  EXPECT_TRUE(NavigateToURL(shell(), target_url, target_url));

  std::string hinted_id = WaitForHintedScriptDevtoolsRequestId();

  {
    base::Value::Dict early_hints_event = WaitForDevtoolsEarlyHints();
    base::Value::Dict* early_hints_headers =
        early_hints_event.FindDict("headers");
    ASSERT_TRUE(early_hints_headers);
    HeaderField link_header = CreatePreloadLinkForScript();
    EXPECT_EQ(*early_hints_headers->FindString(link_header.name),
              link_header.value);
  }

  {
    base::Value::Dict result = WaitForResponseReceived(hinted_id);
    base::Value* from_early_hints_value =
        result.FindByDottedPath("response.fromEarlyHints");
    ASSERT_TRUE(from_early_hints_value);
    EXPECT_TRUE(from_early_hints_value->is_bool());
    EXPECT_TRUE(from_early_hints_value->GetBool());
  }
}

class NavigationEarlyHintsAddressSpaceTest : public NavigationEarlyHintsTest {
 public:
  NavigationEarlyHintsAddressSpaceTest() = default;
  ~NavigationEarlyHintsAddressSpaceTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    NavigationEarlyHintsTest::SetUpCommandLine(command_line);

    private_server_.AddDefaultHandlers(GetTestDataFilePath());
    ASSERT_TRUE(private_server_.Start());

    // Treat the main test server as public for IPAddressSpace tests.
    command_line->AppendSwitchASCII(
        network::switches::kIpAddressSpaceOverrides,
        base::StringPrintf("127.0.0.1:%d=public",
                           net::QuicSimpleTestServer::GetPort()));
  }

  net::test_server::EmbeddedTestServer& private_server() {
    return private_server_;
  }

 private:
  // For tests that trigger private network requests.
  net::EmbeddedTestServer private_server_;
};

// Tests that Early Hints preload is blocked when hints comes from the public
// network but a hinted resource is located in a private network.
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsAddressSpaceTest,
                       PublicToPrivateRequestBlocked) {
  const GURL kPrivateResourceUrl = private_server().GetURL("/blank.jpg");
  ResponseEntry page_entry(kEmptyPagePath, net::HTTP_OK);
  HeaderField link_header = HeaderField(
      "link", base::StringPrintf("<%s>; rel=preload; as=image",
                                 kPrivateResourceUrl.spec().c_str()));
  page_entry.AddEarlyHints({std::move(link_header)});
  RegisterResponse(page_entry);

  EXPECT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  PreloadedResources preloads = WaitForPreloadedResources();
  EXPECT_EQ(preloads.size(), 1UL);

  auto it = preloads.find(kPrivateResourceUrl);
  ASSERT_TRUE(it != preloads.end());
  ASSERT_FALSE(it->second.was_canceled);
  ASSERT_TRUE(it->second.error_code.has_value());
  EXPECT_EQ(it->second.error_code.value(),
            net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS);
  EXPECT_EQ(it->second.cors_error_status->cors_error,
            network::mojom::CorsError::kInsecurePrivateNetwork);
}

class NavigationEarlyHintsPrerenderTest : public NavigationEarlyHintsTest {
 public:
  NavigationEarlyHintsPrerenderTest()
      : prerender_helper_(base::BindRepeating(
            &NavigationEarlyHintsPrerenderTest::web_contents,
            base::Unretained(this))) {}
  ~NavigationEarlyHintsPrerenderTest() override = default;

  test::PrerenderTestHelper* prerender_helper() { return &prerender_helper_; }

  WebContents* web_contents() { return shell()->web_contents(); }

 private:
  test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsPrerenderTest,
                       AllowPreloadInPrerendering) {
  EXPECT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html")));
  ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
  RegisterResponse(entry);

  // Loads a page in the prerender.
  FrameTreeNodeId host_id = prerender_helper()->AddPrerender(
      net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath));
  RenderFrameHostImpl* prerender_rfh = static_cast<RenderFrameHostImpl*>(
      prerender_helper()->GetPrerenderedMainFrameHost(host_id));
  EXPECT_NE(prerender_rfh, nullptr);
  EXPECT_NE(prerender_rfh->early_hints_manager(), nullptr);

  PreloadedResources preloads = WaitForPreloadedResources(prerender_rfh);
  EXPECT_EQ(preloads.size(), 1UL);

  GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
  EXPECT_TRUE(preloads.contains(script_url));
}

class NavigationEarlyHintsFencedFrameTest : public NavigationEarlyHintsTest {
 public:
  NavigationEarlyHintsFencedFrameTest() = default;

  test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

  ResponseEntry CreatePageEntryWithHintedScriptInFencedFrame(
      net::HttpStatusCode status_code) {
    RegisterHintedScriptResource();

    ResponseEntry entry(kPageWithHintedScriptPath, status_code);
    entry.headers["supports-loading-mode"] = "fenced-frame";
    entry.body = kPageWithHintedScriptBody;
    HeaderField link_header = CreatePreloadLinkForScript();
    HeaderField fenced_frame_header =
        HeaderField("supports-loading-mode", "fenced-frame");
    entry.AddEarlyHints(
        {std::move(link_header), std::move(fenced_frame_header)});
    return entry;
  }

 private:
  test::FencedFrameTestHelper fenced_frame_test_helper_;
};

IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsFencedFrameTest,
                       DisallowPreloadInFencedFrame) {
  EXPECT_TRUE(NavigateToURL(
      shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html")));

  ResponseEntry entry =
      CreatePageEntryWithHintedScriptInFencedFrame(net::HTTP_OK);
  RegisterResponse(entry);

  // Create a fenced frame.
  RenderFrameHostImpl* fenced_frame_host = static_cast<RenderFrameHostImpl*>(
      fenced_frame_test_helper().CreateFencedFrame(
          shell()->web_contents()->GetPrimaryMainFrame(),
          net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath)));
  EXPECT_NE(fenced_frame_host, nullptr);
  EXPECT_EQ(fenced_frame_host->early_hints_manager(), nullptr);
}

namespace {

const char kHttp1EarlyHintsPath[] = "/early-hints";

class Http1EarlyHintsResponse : public net::test_server::HttpResponse {
 public:
  Http1EarlyHintsResponse() = default;
  ~Http1EarlyHintsResponse() override = default;

  void SendResponse(
      base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
    base::StringPairs early_hints_headers = {
        {"Link", "</cacheable.js>; rel=preload; as=script"}};
    delegate->SendResponseHeaders(net::HTTP_EARLY_HINTS, "Early Hints",
                                  early_hints_headers);

    base::StringPairs final_response_headers = {
        {"Content-Type", "text/html"},
        {"Link", "</cacheable.js>; rel=preload; as=script"}};
    delegate->SendResponseHeaders(net::HTTP_OK, "OK", final_response_headers);

    delegate->SendContentsAndFinish("<script src=\"cacheable.js\"></script>");
  }
};

std::unique_ptr<net::test_server::HttpResponse> HandleHttpEarlyHintsRequest(
    const net::test_server::HttpRequest& request) {
  const GURL relative_url = request.base_url.Resolve(request.relative_url);
  if (relative_url.GetPath() == kHttp1EarlyHintsPath) {
    return std::make_unique<Http1EarlyHintsResponse>();
  }
  return nullptr;
}

}  // namespace

class NavigationEarlyHintsHttp1Test : public ContentBrowserTest,
                                      public testing::WithParamInterface<bool> {
 public:
  NavigationEarlyHintsHttp1Test() {
    if (EnableEarlyHintsForHttp1()) {
      scoped_feature_list_.InitAndEnableFeature(
          net::features::kEnableEarlyHintsOnHttp11);
    } else {
      scoped_feature_list_.InitAndDisableFeature(
          net::features::kEnableEarlyHintsOnHttp11);
    }
  }

  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    embedded_test_server()->AddDefaultHandlers();
    embedded_test_server()->RegisterRequestHandler(
        base::BindRepeating(&HandleHttpEarlyHintsRequest));
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  bool EnableEarlyHintsForHttp1() { return GetParam(); }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(All, NavigationEarlyHintsHttp1Test, testing::Bool());

// Tests that Early Hints are allowed or disallowed on HTTP/1.1 based on a
// feature flag.
IN_PROC_BROWSER_TEST_P(NavigationEarlyHintsHttp1Test, AllowEarlyHints) {
  const GURL url = embedded_test_server()->GetURL(kHttp1EarlyHintsPath);
  ASSERT_TRUE(NavigateToURL(shell(), url));

  NavigationEarlyHintsManager* early_hints_manager =
      static_cast<RenderFrameHostImpl*>(
          shell()->web_contents()->GetPrimaryMainFrame())
          ->early_hints_manager();
  if (EnableEarlyHintsForHttp1()) {
    ASSERT_TRUE(early_hints_manager->WasResourceHintsReceived());
  } else {
    ASSERT_TRUE(early_hints_manager == nullptr);
  }
}

}  // namespace content