Added Sec-Purpose header to prefetch requests.

This CL adds the "Sec-Purpose" header which is added to any prefetch
request. Direct prefetch requests that do not use the prefetch proxy
will have a value of "prefetch", and prefetch requests that do use the
prefetch proxy will have a value of "prefetch;anonymous-client-ip". This
will allow servers to differentiate between the two types of prefetch
requests.

Bug: 1290561
Change-Id: I24928fd75b6a55f92109022e94beeca54b53e2b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3421706
Reviewed-by: Robert Ogden <[email protected]>
Commit-Queue: Max Curran <[email protected]>
Cr-Commit-Position: refs/heads/main@{#965381}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 6cc814c..3911d9c7 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1258,6 +1258,8 @@
     "prefetch/no_state_prefetch/no_state_prefetch_tab_helper.h",
     "prefetch/pref_names.cc",
     "prefetch/pref_names.h",
+    "prefetch/prefetch_headers.cc",
+    "prefetch/prefetch_headers.h",
     "prefetch/prefetch_prefs.cc",
     "prefetch/prefetch_prefs.h",
     "prefetch/prefetch_proxy/chrome_speculation_host_delegate.cc",
diff --git a/chrome/browser/prefetch/prefetch_headers.cc b/chrome/browser/prefetch/prefetch_headers.cc
new file mode 100644
index 0000000..554daf6
--- /dev/null
+++ b/chrome/browser/prefetch/prefetch_headers.cc
@@ -0,0 +1,22 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/prefetch/prefetch_headers.h"
+
+namespace prefetch::headers {
+
+// The header used to indicate the purpose of a request. It should have one of
+// the two values below.
+const char kSecPurposeHeaderName[] = "Sec-Purpose";
+
+// This value indicates that the request is a prefetch request made directly to
+// the server.
+const char kSecPurposePrefetchHeaderValue[] = "prefetch";
+
+// This value indicates that the request is a prefetch request made via an
+// anonymous client IP proxy.
+const char kSecPurposePrefetchAnonymousClientIpHeaderValue[] =
+    "prefetch;anonymous-client-ip";
+
+}  // namespace prefetch::headers
diff --git a/chrome/browser/prefetch/prefetch_headers.h b/chrome/browser/prefetch/prefetch_headers.h
new file mode 100644
index 0000000..591fed7
--- /dev/null
+++ b/chrome/browser/prefetch/prefetch_headers.h
@@ -0,0 +1,18 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_PREFETCH_PREFETCH_HEADERS_H_
+#define CHROME_BROWSER_PREFETCH_PREFETCH_HEADERS_H_
+
+namespace prefetch::headers {
+
+extern const char kSecPurposeHeaderName[];
+
+extern const char kSecPurposePrefetchHeaderValue[];
+
+extern const char kSecPurposePrefetchAnonymousClientIpHeaderValue[];
+
+}  // namespace prefetch::headers
+
+#endif  // CHROME_BROWSER_PREFETCH_PREFETCH_HEADERS_H_
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_browsertest.cc b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_browsertest.cc
index 5a53dd66..02c3652 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_browsertest.cc
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_browsertest.cc
@@ -769,6 +769,31 @@
     EXPECT_EQ(paths.size(), verified_url_count);
   }
 
+  // Verifies that the "Sec-Purpose" header with the expected value
+  // ("prefetch;anonymous-client-ip" for requests that go through the proxy, and
+  // "prefetch" for non-private prefetches) was included on all requests for
+  // main resources. Subresources are fetched using NSP which does not add the
+  // "Sec-Purpose" header.
+  void VerifyPrefetchRequestsSecPurposeHeader(
+      const std::set<std::string>& main_resource_paths,
+      bool are_requests_anonymous_client_ip) {
+    size_t verified_header_count = 0;
+    for (const auto& request : origin_server_requests()) {
+      const GURL& url = request.GetURL();
+      if (main_resource_paths.find(url.path()) == main_resource_paths.end()) {
+        continue;
+      }
+
+      SCOPED_TRACE(request.GetURL().spec());
+      EXPECT_EQ(request.headers.find("Sec-Purpose")->second,
+                are_requests_anonymous_client_ip
+                    ? "prefetch;anonymous-client-ip"
+                    : "prefetch");
+      verified_header_count++;
+    }
+    EXPECT_EQ(main_resource_paths.size(), verified_header_count);
+  }
+
   size_t OriginServerRequestCount() const {
     base::RunLoop().RunUntilIdle();
     return origin_server_request_count_;
@@ -823,7 +848,11 @@
 
     bool is_prefetch =
         request.headers.find("Purpose") != request.headers.end() &&
-        request.headers.find("Purpose")->second == "prefetch";
+        request.headers.find("Purpose")->second == "prefetch" &&
+        request.headers.find("Sec-Purpose") != request.headers.end() &&
+        (request.headers.find("Sec-Purpose")->second == "prefetch" ||
+         request.headers.find("Sec-Purpose")->second ==
+             "prefetch;anonymous-client-ip");
 
     if (request.relative_url == "/404_on_prefetch") {
       std::unique_ptr<net::test_server::BasicHttpResponse> resp =
@@ -1362,6 +1391,9 @@
   EXPECT_EQ(u"Title Of Awesomeness", GetWebContents()->GetTitle());
 
   VerifyOriginRequestsAreIsolated({prefetch_url.path()});
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {prefetch_url.path()},
+      /*are_requests_anonymous_client_ip=*/true);
 
   // The origin server should not have served this request.
   EXPECT_EQ(starting_origin_request_count, OriginServerRequestCount());
@@ -1429,6 +1461,14 @@
       eligible_link_3.path(),
   });
 
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {
+          eligible_link_1.path(),
+          eligible_link_2.path(),
+          eligible_link_3.path(),
+      },
+      /*are_requests_anonymous_client_ip=*/true);
+
   using UkmEntry = ukm::TestUkmRecorder::HumanReadableUkmEntry;
   auto expected_entries = std::vector<UkmEntry>{
       // eligible_link_1
@@ -2997,6 +3037,9 @@
       "/prefetch/prefetch_proxy/prefetch.js",
       eligible_link.path(),
   });
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {eligible_link.path()},
+      /*are_requests_anonymous_client_ip=*/true);
 
   // Verify the resource load was reported to the subresource manager.
   PrefetchProxyService* service =
@@ -4149,6 +4192,9 @@
       "/prefetch/prefetch_proxy/prefetch.js",
       eligible_link.path(),
   });
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {eligible_link.path()},
+      /*are_requests_anonymous_client_ip=*/true);
 
   // Verify the resource load was reported to the subresource manager.
   PrefetchProxyService* service =
@@ -4270,6 +4316,9 @@
   EXPECT_EQ(u"Title Of Awesomeness", GetWebContents()->GetTitle());
 
   VerifyOriginRequestsAreIsolated({prefetch_url.path()});
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {prefetch_url.path()},
+      /*are_requests_anonymous_client_ip=*/true);
 
   // The origin server should not have served this request.
   EXPECT_EQ(starting_origin_request_count, OriginServerRequestCount());
@@ -4702,6 +4751,10 @@
             content::GetCookies(
                 browser()->profile(), eligible_link,
                 net::CookieOptions::SameSiteCookieContext::MakeInclusive()));
+
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {eligible_link.path()},
+      /*are_requests_anonymous_client_ip=*/false);
 }
 
 IN_PROC_BROWSER_TEST_F(
@@ -4742,7 +4795,7 @@
 
   // The prefetch requests shouldn't use the proxy and should  go directly to
   // the origin server.
-  EXPECT_EQ(proxy_server_requests().size(), (unsigned int)0);
+  EXPECT_EQ(proxy_server_requests().size(), 0U);
 
   // The prefetch should be made using the default network context, so the
   // cookie should be present once the prefetch is complete.
@@ -4765,4 +4818,8 @@
       "PrefetchProxy.AfterClick.Mainframe.CookieWaitTime", 0, 1);
   histogram_tester.ExpectUniqueSample(
       "PrefetchProxy.Prefetch.Mainframe.CookiesToCopy", 0, 1);
+
+  VerifyPrefetchRequestsSecPurposeHeader(
+      {eligible_link.path()},
+      /*are_requests_anonymous_client_ip=*/false);
 }
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_tab_helper.cc b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_tab_helper.cc
index 9b33a80d..c0f65a3 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_tab_helper.cc
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_tab_helper.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/chrome_content_browser_client.h"
 #include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
 #include "chrome/browser/prefetch/no_state_prefetch/no_state_prefetch_manager_factory.h"
+#include "chrome/browser/prefetch/prefetch_headers.h"
 #include "chrome/browser/prefetch/prefetch_prefs.h"
 #include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_features.h"
 #include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_network_context_client.h"
@@ -836,6 +837,11 @@
   request->load_flags = net::LOAD_DISABLE_CACHE | net::LOAD_PREFETCH;
   request->credentials_mode = network::mojom::CredentialsMode::kInclude;
   request->headers.SetHeader(content::kCorsExemptPurposeHeaderName, "prefetch");
+  request->headers.SetHeader(
+      prefetch::headers::kSecPurposeHeaderName,
+      prefetch_container->GetPrefetchType().IsProxyRequired()
+          ? prefetch::headers::kSecPurposePrefetchAnonymousClientIpHeaderValue
+          : prefetch::headers::kSecPurposePrefetchHeaderValue);
   // Remove the user agent header if it was set so that the network context's
   // default is used.
   request->headers.RemoveHeader("User-Agent");
diff --git a/chrome/browser/prefetch/search_prefetch/base_search_prefetch_request.cc b/chrome/browser/prefetch/search_prefetch/base_search_prefetch_request.cc
index 8fa921d..76e16d45 100644
--- a/chrome/browser/prefetch/search_prefetch/base_search_prefetch_request.cc
+++ b/chrome/browser/prefetch/search_prefetch/base_search_prefetch_request.cc
@@ -7,6 +7,7 @@
 #include <vector>
 
 #include "build/build_config.h"
+#include "chrome/browser/prefetch/prefetch_headers.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/search_engines/template_url_service_factory.h"
 #include "chrome/common/pref_names.h"
@@ -189,6 +190,9 @@
   resource_request->headers.SetHeader(content::kCorsExemptPurposeHeaderName,
                                       "prefetch");
   resource_request->headers.SetHeader(
+      prefetch::headers::kSecPurposeHeaderName,
+      prefetch::headers::kSecPurposePrefetchHeaderValue);
+  resource_request->headers.SetHeader(
       net::HttpRequestHeaders::kAccept,
       content::FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, profile));
 
diff --git a/chrome/browser/prefetch/search_prefetch/search_prefetch_service_browsertest.cc b/chrome/browser/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
index 67633e0..17123c988 100644
--- a/chrome/browser/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
+++ b/chrome/browser/prefetch/search_prefetch/search_prefetch_service_browsertest.cc
@@ -455,7 +455,9 @@
 
     bool is_prefetch =
         request.headers.find("Purpose") != request.headers.end() &&
-        request.headers.find("Purpose")->second == "prefetch";
+        request.headers.find("Purpose")->second == "prefetch" &&
+        request.headers.find("Sec-Purpose") != request.headers.end() &&
+        request.headers.find("Sec-Purpose")->second == "prefetch";
 
     content::GetUIThreadTaskRunner({})->PostTask(
         FROM_HERE,