Route conversion registration requests from blink to ConversionStorage
This change implements a conversion registration flow for the Event Level
Conversion Measurement API.
A request is considered a conversion registration if it redirects to
/.well-known/register-conversion. This request is blocked in
frame_fetch_context and sent via a NavigationAssociatedInterface to
ConversionManager and logged to storage.
Design doc(RVG): https://crbug.com/1039466
Explainer: https://github.com/csharrison/conversion-measurement-api
Bug: 1042923
Change-Id: If0398a662f68cc6092922c79ba05f546bf0562e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1998977
Commit-Queue: John Delaney <[email protected]>
Reviewed-by: Charlie Harrison <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Reviewed-by: Mike West <[email protected]>
Cr-Commit-Position: refs/heads/master@{#739602}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index d4098d7..c2db18f 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -672,6 +672,8 @@
"content_index/content_index_service_impl.h",
"content_service_delegate_impl.cc",
"content_service_delegate_impl.h",
+ "conversions/conversion_host.cc",
+ "conversions/conversion_host.h",
"conversions/conversion_manager.cc",
"conversions/conversion_manager.h",
"conversions/conversion_policy.cc",
diff --git a/content/browser/conversions/conversion_host.cc b/content/browser/conversions/conversion_host.cc
new file mode 100644
index 0000000..ec917fa
--- /dev/null
+++ b/content/browser/conversions/conversion_host.cc
@@ -0,0 +1,77 @@
+// Copyright 2020 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 "content/browser/conversions/conversion_host.h"
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "content/browser/conversions/conversion_manager.h"
+#include "content/browser/conversions/conversion_policy.h"
+#include "content/browser/conversions/storable_conversion.h"
+#include "content/browser/storage_partition_impl.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/message.h"
+#include "services/network/public/cpp/is_potentially_trustworthy.h"
+#include "url/origin.h"
+
+namespace content {
+
+ConversionHost::ConversionHost(WebContents* contents)
+ : web_contents_(contents), receiver_(contents, this) {}
+
+ConversionHost::~ConversionHost() = default;
+
+// TODO(https://crbug.com/1044099): Limit the number of conversion redirects per
+// page-load to a reasonable number.
+void ConversionHost::RegisterConversion(
+ blink::mojom::ConversionPtr conversion) {
+ // If there is no conversion manager available, ignore any conversion
+ // registrations.
+ if (!GetManager())
+ return;
+ content::RenderFrameHost* render_frame_host =
+ receiver_.GetCurrentTargetFrame();
+
+ // Conversion registration is only allowed in the main frame.
+ if (render_frame_host->GetParent()) {
+ mojo::ReportBadMessage(
+ "blink.mojom.ConversionHost can only be used by the main frame.");
+ return;
+ }
+
+ // Only allow conversion registration on secure pages with a secure conversion
+ // redirects.
+ if (!network::IsOriginPotentiallyTrustworthy(
+ render_frame_host->GetLastCommittedOrigin()) ||
+ !network::IsOriginPotentiallyTrustworthy(conversion->reporting_origin)) {
+ mojo::ReportBadMessage(
+ "blink.mojom.ConversionHost can only be used in secure contexts with a "
+ "secure conversion registration origin.");
+ return;
+ }
+
+ StorableConversion storable_conversion(
+ GetManager()->GetConversionPolicy().GetSanitizedConversionData(
+ conversion->conversion_data),
+ render_frame_host->GetLastCommittedOrigin(),
+ conversion->reporting_origin);
+
+ GetManager()->HandleConversion(storable_conversion);
+}
+
+void ConversionHost::SetCurrentTargetFrameForTesting(
+ RenderFrameHost* render_frame_host) {
+ receiver_.SetCurrentTargetFrameForTesting(render_frame_host);
+}
+
+ConversionManager* ConversionHost::GetManager() {
+ return static_cast<StoragePartitionImpl*>(
+ BrowserContext::GetDefaultStoragePartition(
+ web_contents_->GetBrowserContext()))
+ ->GetConversionManager();
+}
+
+} // namespace content
diff --git a/content/browser/conversions/conversion_host.h b/content/browser/conversions/conversion_host.h
new file mode 100644
index 0000000..468fa6b5
--- /dev/null
+++ b/content/browser/conversions/conversion_host.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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 CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_
+#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_
+
+#include "base/gtest_prod_util.h"
+#include "content/public/browser/web_contents_receiver_set.h"
+#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
+
+namespace content {
+
+class ConversionManager;
+class RenderFrameHost;
+class WebContents;
+
+// Class responsible for listening to conversion events originating from blink,
+// and verifying that they are valid. Owned by the WebContents. Lifetime is
+// bound to lifetime of the WebContents.
+class CONTENT_EXPORT ConversionHost : public blink::mojom::ConversionHost {
+ public:
+ explicit ConversionHost(WebContents* web_contents);
+ ConversionHost(const ConversionHost& other) = delete;
+ ConversionHost& operator=(const ConversionHost& other) = delete;
+ ~ConversionHost() override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, ConversionInSubframe_BadMessage);
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest,
+ ConversionOnInsecurePage_BadMessage);
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest,
+ ConversionWithInsecureReportingOrigin_BadMessage);
+ FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, ValidConversion_NoBadMessage);
+
+ // blink::mojom::ConversionHost:
+ void RegisterConversion(blink::mojom::ConversionPtr conversion) override;
+
+ // Sets the target frame on |receiver_|.
+ void SetCurrentTargetFrameForTesting(RenderFrameHost* render_frame_host);
+
+ // Gets the manager for this web contents. Can be null.
+ ConversionManager* GetManager();
+
+ WebContents* web_contents_;
+
+ WebContentsFrameReceiverSet<blink::mojom::ConversionHost> receiver_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_
diff --git a/content/browser/conversions/conversion_host_unittest.cc b/content/browser/conversions/conversion_host_unittest.cc
new file mode 100644
index 0000000..255587e9
--- /dev/null
+++ b/content/browser/conversions/conversion_host_unittest.cc
@@ -0,0 +1,126 @@
+// Copyright 2020 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 "content/browser/conversions/conversion_host.h"
+
+#include <memory>
+
+#include "base/test/scoped_feature_list.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/fake_mojo_message_dispatch_context.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
+#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace content {
+
+class ConversionHostTest : public RenderViewHostTestHarness {
+ public:
+ ConversionHostTest() {
+ feature_list_.InitAndEnableFeature(features::kConversionMeasurement);
+ }
+
+ void SetUp() override {
+ RenderViewHostTestHarness::SetUp();
+ static_cast<WebContentsImpl*>(web_contents())
+ ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_);
+ conversion_host_ = std::make_unique<ConversionHost>(web_contents());
+ contents()->GetMainFrame()->InitializeRenderFrameIfNeeded();
+ }
+
+ TestWebContents* contents() {
+ return static_cast<TestWebContents*>(web_contents());
+ }
+
+ ConversionHost* conversion_host() { return conversion_host_.get(); }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ std::unique_ptr<ConversionHost> conversion_host_;
+};
+
+TEST_F(ConversionHostTest, ConversionInSubframe_BadMessage) {
+ contents()->NavigateAndCommit(GURL("http://www.example.com"));
+
+ // Create a subframe and use it as a target for the conversion registration
+ // mojo.
+ content::RenderFrameHostTester* rfh_tester =
+ content::RenderFrameHostTester::For(main_rfh());
+ content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
+ conversion_host()->SetCurrentTargetFrameForTesting(subframe);
+
+ // Create a fake dispatch context to trigger a bad message in.
+ FakeMojoMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+
+ // Message should be ignored because it was registered from a subframe.
+ conversion_host()->RegisterConversion(std::move(conversion));
+ EXPECT_EQ("blink.mojom.ConversionHost can only be used by the main frame.",
+ bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(ConversionHostTest, ConversionOnInsecurePage_BadMessage) {
+ // Create a page with an insecure origin.
+ contents()->NavigateAndCommit(GURL("http://www.example.com"));
+ conversion_host()->SetCurrentTargetFrameForTesting(main_rfh());
+
+ FakeMojoMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+ conversion->reporting_origin =
+ url::Origin::Create(GURL("https://secure.com"));
+
+ // Message should be ignored because it was registered from an insecure page.
+ conversion_host()->RegisterConversion(std::move(conversion));
+ EXPECT_EQ(
+ "blink.mojom.ConversionHost can only be used in secure contexts with a "
+ "secure conversion registration origin.",
+ bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) {
+ contents()->NavigateAndCommit(GURL("https://www.example.com"));
+ conversion_host()->SetCurrentTargetFrameForTesting(main_rfh());
+
+ FakeMojoMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+ conversion->reporting_origin = url::Origin::Create(GURL("http://secure.com"));
+
+ // Message should be ignored because it was registered with an insecure
+ // redirect.
+ conversion_host()->RegisterConversion(std::move(conversion));
+ EXPECT_EQ(
+ "blink.mojom.ConversionHost can only be used in secure contexts with a "
+ "secure conversion registration origin.",
+ bad_message_observer.WaitForBadMessage());
+}
+
+TEST_F(ConversionHostTest, ValidConversion_NoBadMessage) {
+ // Create a page with an insecure origin.
+ contents()->NavigateAndCommit(GURL("https://www.example.com"));
+ conversion_host()->SetCurrentTargetFrameForTesting(main_rfh());
+
+ // Create a fake dispatch context to trigger a bad message in.
+ FakeMojoMessageDispatchContext fake_dispatch_context;
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+ conversion->reporting_origin =
+ url::Origin::Create(GURL("https://secure.com"));
+ conversion_host()->RegisterConversion(std::move(conversion));
+
+ // Run loop to allow the bad message code to run if a bad message was
+ // triggered.
+ base::RunLoop().RunUntilIdle();
+ EXPECT_FALSE(bad_message_observer.got_bad_message());
+}
+
+} // namespace content
diff --git a/content/browser/conversions/conversion_manager.cc b/content/browser/conversions/conversion_manager.cc
index dad2f88..2e35885 100644
--- a/content/browser/conversions/conversion_manager.cc
+++ b/content/browser/conversions/conversion_manager.cc
@@ -34,6 +34,22 @@
ConversionManager::~ConversionManager() = default;
+void ConversionManager::HandleConversion(const StorableConversion& conversion) {
+ if (!storage_)
+ return;
+
+ // TODO(https://crbug.com/1043345): Add UMA for the number of conversions we
+ // are logging to storage, and the number of new reports logged to storage.
+ // Unretained is safe because any task to delete |storage_| will be posted
+ // after this one.
+ storage_task_runner_.get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(
+ base::IgnoreResult(
+ &ConversionStorage::MaybeCreateAndStoreConversionReports),
+ base::Unretained(storage_.get()), conversion));
+}
+
const ConversionPolicy& ConversionManager::GetConversionPolicy() const {
return *conversion_policy_;
}
diff --git a/content/browser/conversions/conversion_manager.h b/content/browser/conversions/conversion_manager.h
index 64eb19e..fe132c4 100644
--- a/content/browser/conversions/conversion_manager.h
+++ b/content/browser/conversions/conversion_manager.h
@@ -14,6 +14,7 @@
#include "base/sequenced_task_runner.h"
#include "content/browser/conversions/conversion_policy.h"
#include "content/browser/conversions/conversion_storage.h"
+#include "content/browser/conversions/storable_conversion.h"
namespace base {
class Clock;
@@ -35,6 +36,10 @@
ConversionManager& operator=(const ConversionManager& other) = delete;
~ConversionManager() override;
+ // Process a newly registered conversion. Will create and log any new
+ // conversion reports to storage.
+ void HandleConversion(const StorableConversion& conversion);
+
const ConversionPolicy& GetConversionPolicy() const;
private:
diff --git a/content/browser/conversions/conversion_policy.cc b/content/browser/conversions/conversion_policy.cc
index 9b21031..d323ccf 100644
--- a/content/browser/conversions/conversion_policy.cc
+++ b/content/browser/conversions/conversion_policy.cc
@@ -4,11 +4,52 @@
#include "content/browser/conversions/conversion_policy.h"
+#include "base/format_macros.h"
#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/rand_util.h"
+#include "base/strings/stringprintf.h"
#include "base/time/time.h"
namespace content {
+namespace {
+
+// Maximum number of allowed conversion metadata values. Higher entropy
+// conversion metadata is stripped to these lower bits.
+const int kMaxAllowedConversionValues = 8;
+
+} // namespace
+
+uint64_t ConversionPolicy::NoiseProvider::GetNoisedConversionData(
+ uint64_t conversion_data) const {
+ // Return |conversion_data| without any noise 95% of the time.
+ if (base::RandDouble() > .05)
+ return conversion_data;
+
+ // 5% of the time return a random number in the allowed range. Note that the
+ // value is noised 5% of the time, but only wrong 5 *
+ // (kMaxAllowedConversionValues - 1) / kMaxAllowedConversionValues percent of
+ // the time.
+ return static_cast<uint64_t>(base::RandInt(0, kMaxAllowedConversionValues));
+}
+
+// static
+std::unique_ptr<ConversionPolicy> ConversionPolicy::CreateForTesting(
+ std::unique_ptr<NoiseProvider> noise_provider) {
+ return base::WrapUnique<ConversionPolicy>(
+ new ConversionPolicy(std::move(noise_provider)));
+}
+
+ConversionPolicy::ConversionPolicy()
+ : noise_provider_(std::make_unique<NoiseProvider>()) {}
+
+ConversionPolicy::ConversionPolicy(
+ std::unique_ptr<ConversionPolicy::NoiseProvider> noise_provider)
+ : noise_provider_(std::move(noise_provider)) {}
+
+ConversionPolicy::~ConversionPolicy() = default;
+
base::Time ConversionPolicy::GetReportTimeForConversion(
const ConversionReport& report) const {
// After the initial impression, a schedule of reporting windows and deadlines
@@ -85,4 +126,19 @@
last_report->attribution_credit = 100;
}
+std::string ConversionPolicy::GetSanitizedConversionData(
+ uint64_t conversion_data) const {
+ // Add noise to the conversion when the value is first sanitized from a
+ // conversion registration event. This noised data will be used for all
+ // associated impressions that convert.
+ conversion_data = noise_provider_->GetNoisedConversionData(conversion_data);
+
+ // Allow at most 3 bits of entropy in conversion data. base::StringPrintf() is
+ // used over base::HexEncode() because HexEncode() returns a hex string with
+ // little-endian ordering. Big-endian ordering is expected here because the
+ // API assumes big-endian when parsing attributes.
+ return base::StringPrintf("%" PRIx64,
+ conversion_data % kMaxAllowedConversionValues);
+}
+
} // namespace content
diff --git a/content/browser/conversions/conversion_policy.h b/content/browser/conversions/conversion_policy.h
index 28031c5..1a14eac9 100644
--- a/content/browser/conversions/conversion_policy.h
+++ b/content/browser/conversions/conversion_policy.h
@@ -5,6 +5,9 @@
#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_POLICY_H_
#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_POLICY_H_
+#include <stdint.h>
+#include <memory>
+#include <string>
#include <vector>
#include "base/time/time.h"
@@ -17,10 +20,26 @@
// storing, and sending impressions and conversions.
class CONTENT_EXPORT ConversionPolicy {
public:
- ConversionPolicy() = default;
+ // Helper class that generates noised conversion data. Can be overridden to
+ // make testing deterministic.
+ class CONTENT_EXPORT NoiseProvider {
+ public:
+ NoiseProvider() = default;
+ virtual ~NoiseProvider() = default;
+
+ // Returns a noise value of |conversion_data|. By default, this reports
+ // completely random data for 5% of conversions, and sends the real data for
+ // 95%. Virtual for testing.
+ virtual uint64_t GetNoisedConversionData(uint64_t conversion_data) const;
+ };
+
+ static std::unique_ptr<ConversionPolicy> CreateForTesting(
+ std::unique_ptr<NoiseProvider> noise_provider);
+
+ ConversionPolicy();
ConversionPolicy(const ConversionPolicy& other) = delete;
ConversionPolicy& operator=(const ConversionPolicy& other) = delete;
- virtual ~ConversionPolicy() = default;
+ virtual ~ConversionPolicy();
// Get the time a conversion report should be sent, by batching reports into
// set reporting windows based on their impression time. This strictly delays
@@ -37,6 +56,17 @@
// for the most recent impression a credit of 100, and the rest a credit of 0.
virtual void AssignAttributionCredits(
std::vector<ConversionReport>* reports) const;
+
+ // Gets the sanitized conversion data for a conversion. This strips entropy
+ // from the provided to data to at most 3 bits of information.
+ virtual std::string GetSanitizedConversionData(
+ uint64_t conversion_data) const;
+
+ private:
+ // For testing only.
+ ConversionPolicy(std::unique_ptr<NoiseProvider> noise_provider);
+
+ std::unique_ptr<NoiseProvider> noise_provider_;
};
} // namespace content
diff --git a/content/browser/conversions/conversion_policy_unittest.cc b/content/browser/conversions/conversion_policy_unittest.cc
index 805ad9b..b2e5941 100644
--- a/content/browser/conversions/conversion_policy_unittest.cc
+++ b/content/browser/conversions/conversion_policy_unittest.cc
@@ -4,8 +4,10 @@
#include "content/browser/conversions/conversion_policy.h"
+#include <memory>
#include <vector>
+#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "content/browser/conversions/conversion_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -25,6 +27,22 @@
/*conversion_id=*/base::nullopt);
}
+// Fake ConversionNoiseProvider that return un-noised conversion data.
+class EmptyNoiseProvider : public ConversionPolicy::NoiseProvider {
+ public:
+ uint64_t GetNoisedConversionData(uint64_t conversion_data) const override {
+ return conversion_data;
+ }
+};
+
+// Mock ConversionNoiseProvider that always noises values by +1.
+class IncrementingNoiseProvider : public ConversionPolicy::NoiseProvider {
+ public:
+ uint64_t GetNoisedConversionData(uint64_t conversion_data) const override {
+ return conversion_data + 1;
+ }
+};
+
} // namespace
class ConversionPolicyTest : public testing::Test {
@@ -32,6 +50,31 @@
ConversionPolicyTest() = default;
};
+TEST_F(ConversionPolicyTest, HighEntropyConversionData_StrippedToLowerBits) {
+ uint64_t conversion_data = 8LU;
+
+ // The policy should strip the data to the lower 3 bits.
+ EXPECT_EQ("0", ConversionPolicy::CreateForTesting(
+ std::make_unique<EmptyNoiseProvider>())
+ ->GetSanitizedConversionData(conversion_data));
+}
+
+TEST_F(ConversionPolicyTest, ThreeBitConversionData_Unchanged) {
+ std::unique_ptr<ConversionPolicy> policy = ConversionPolicy::CreateForTesting(
+ std::make_unique<EmptyNoiseProvider>());
+ for (uint64_t conversion_data = 0; conversion_data < 8; conversion_data++) {
+ EXPECT_EQ(base::NumberToString(conversion_data),
+ policy->GetSanitizedConversionData(conversion_data));
+ }
+}
+
+TEST_F(ConversionPolicyTest, SantizizeConversionData_OutputHasNoise) {
+ // The policy should include noise when sanitizing data.
+ EXPECT_EQ("5", ConversionPolicy::CreateForTesting(
+ std::make_unique<IncrementingNoiseProvider>())
+ ->GetSanitizedConversionData(4UL));
+}
+
TEST_F(ConversionPolicyTest, ImmediateConversion_FirstWindowUsed) {
base::Time impression_time = base::Time::Now();
auto report = GetReport(impression_time, /*conversion_time=*/impression_time);
diff --git a/content/browser/conversions/conversion_registration_browsertest.cc b/content/browser/conversions/conversion_registration_browsertest.cc
new file mode 100644
index 0000000..55d40c2
--- /dev/null
+++ b/content/browser/conversions/conversion_registration_browsertest.cc
@@ -0,0 +1,270 @@
+// Copyright 2020 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 <stdint.h>
+#include <memory>
+
+#include "base/bind.h"
+#include "base/test/scoped_feature_list.h"
+#include "content/browser/conversions/conversion_host.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/common/content_features.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/shell/browser/shell.h"
+#include "content/test/resource_load_observer.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+// Well known path for registering conversions.
+const std::string kWellKnownUrl = ".well-known/register-conversion";
+
+} // namespace
+
+// A mock conversion host which waits for a conversion registration
+// mojo message is received. Tracks the last seen conversion data.
+class TestConversionHost : public ConversionHost {
+ public:
+ static std::unique_ptr<TestConversionHost> ReplaceAndGetConversionHost(
+ WebContents* contents) {
+ static_cast<WebContentsImpl*>(contents)->RemoveReceiverSetForTesting(
+ blink::mojom::ConversionHost::Name_);
+ return std::make_unique<TestConversionHost>(contents);
+ }
+
+ explicit TestConversionHost(WebContents* contents)
+ : ConversionHost(contents) {}
+
+ void RegisterConversion(blink::mojom::ConversionPtr conversion) override {
+ last_conversion_data_ = conversion->conversion_data;
+ num_conversions_++;
+
+ // Don't quit the run loop if we have not seen the expected number of
+ // conversions.
+ if (num_conversions_ < expected_num_conversions_)
+ return;
+ conversion_waiter_.Quit();
+ }
+
+ // Returns the last conversion data after |expected_num_conversions| have been
+ // observed.
+ uint64_t WaitForNumConversions(size_t expected_num_conversions) {
+ if (expected_num_conversions == num_conversions_)
+ return last_conversion_data_;
+ expected_num_conversions_ = expected_num_conversions;
+ conversion_waiter_.Run();
+ return last_conversion_data_;
+ }
+
+ size_t num_conversions() { return num_conversions_; }
+
+ private:
+ uint64_t last_conversion_data_ = 0;
+ size_t num_conversions_ = 0;
+ size_t expected_num_conversions_ = 0;
+ base::RunLoop conversion_waiter_;
+};
+
+class ConversionRegistrationBrowserTest : public ContentBrowserTest {
+ public:
+ ConversionRegistrationBrowserTest() {
+ feature_list_.InitAndEnableFeature(features::kConversionMeasurement);
+ }
+
+ void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
+ embedded_test_server()->ServeFilesFromSourceDirectory(
+ "content/test/data/conversions");
+ SetupCrossSiteRedirector(embedded_test_server());
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ https_server_ = std::make_unique<net::EmbeddedTestServer>(
+ net::EmbeddedTestServer::TYPE_HTTPS);
+ https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
+ net::test_server::RegisterDefaultHandlers(https_server_.get());
+ https_server_->ServeFilesFromSourceDirectory(
+ "content/test/data/conversions");
+ SetupCrossSiteRedirector(https_server_.get());
+ ASSERT_TRUE(https_server_->Start());
+ }
+
+ WebContents* web_contents() { return shell()->web_contents(); }
+
+ net::EmbeddedTestServer* https_server() { return https_server_.get(); }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ std::unique_ptr<net::EmbeddedTestServer> https_server_;
+};
+
+// Test that full conversion path does not cause any failure when a conversion
+// registration mojo is received.
+// TODO(johnidel): This should really be testing internals of ConversionHost.
+// That is trivial when reporting for conversions is added.
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegistration_NoCrash) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(),
+ embedded_test_server()->GetURL("/page_with_conversion_redirect.html")));
+ EXPECT_TRUE(ExecJs(web_contents(), "createTrackingPixel(\"" + kWellKnownUrl +
+ "?conversion-data=100\");"));
+
+ ASSERT_NO_FATAL_FAILURE(
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank"))));
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegistered_ConversionDataReceived) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(),
+ embedded_test_server()->GetURL("/page_with_conversion_redirect.html")));
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ EXPECT_TRUE(ExecJs(web_contents(), "registerConversion(123)"));
+ EXPECT_EQ(123UL, host->WaitForNumConversions(1));
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegistrationNotRedirect_NotReceived) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(),
+ embedded_test_server()->GetURL("/page_with_conversion_redirect.html")));
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ GURL registration_url = embedded_test_server()->GetURL(
+ "/" + kWellKnownUrl + "?conversion-data=200");
+
+ // Create a load observer that will wait for the redirect to complete. If a
+ // conversion was registered, this redirect would never complete.
+ ResourceLoadObserver load_observer(shell());
+ EXPECT_TRUE(ExecJs(web_contents(),
+ JsReplace("createTrackingPixel($1);", registration_url)));
+ load_observer.WaitForResourceCompletion(registration_url);
+
+ // Conversion mojo messages are sent on the same message pipe as navigation
+ // messages. Because the conversion would have been sequenced prior to the
+ // navigation message, it would be observed before the NavigateToURL() call
+ // finishes.
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+ EXPECT_EQ(0u, host->num_conversions());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegistrationInPreload_NotReceived) {
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+ EXPECT_TRUE(
+ NavigateToURL(shell(), embedded_test_server()->GetURL(
+ "/page_with_preload_conversion_ping.html")));
+
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+ EXPECT_EQ(0u, host->num_conversions());
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegistrationNoData_ReceivedZero) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(),
+ embedded_test_server()->GetURL("/page_with_conversion_redirect.html")));
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ EXPECT_TRUE(ExecJs(web_contents(), "createTrackingPixel(\"server-redirect?" +
+ kWellKnownUrl + "\");"));
+
+ // Conversion data should be defaulted to 0.
+ EXPECT_EQ(0UL, host->WaitForNumConversions(1));
+}
+
+IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest,
+ ConversionRegisteredFromChildFrame_NotReceived) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(),
+ embedded_test_server()->GetURL("/page_with_subframe_conversion.html")));
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ GURL redirect_url = embedded_test_server()->GetURL(
+ "/server-redirect?" + kWellKnownUrl + "?conversion-data=200");
+ ResourceLoadObserver load_observer(shell());
+ EXPECT_TRUE(ExecJs(ChildFrameAt(web_contents()->GetMainFrame(), 0),
+ JsReplace("createTrackingPixel($1);", redirect_url)));
+ load_observer.WaitForResourceCompletion(redirect_url);
+
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+ EXPECT_EQ(0u, host->num_conversions());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ ConversionRegistrationBrowserTest,
+ RegisterWithDifferentUrlTypes_ConversionReceivedOrIgnored) {
+ const char kSecureHost[] = "a.test";
+ struct {
+ std::string page_host;
+ std::string redirect_host;
+ bool expected_conversion;
+ } kTestCases[] = {
+ {"localhost" /* page_host */, "localhost" /* redirect_host */,
+ true /* conversion_expected */},
+ {"127.0.0.1" /* page_host */, "127.0.0.1" /* redirect_host */,
+ true /* conversion_expected */},
+ {"insecure.com" /* page_host */, "insecure.com" /* redirect_host */,
+ false /* conversion_expected */},
+ {kSecureHost /* page_host */, kSecureHost /* redirect_host */,
+ true /* conversion_expected */},
+ {"insecure.com" /* page_host */, kSecureHost /* redirect_host */,
+ false /* conversion_expected */},
+ {kSecureHost /* page_host */, "insecure.com" /* redirect_host */,
+ false /* conversion_expected */}};
+
+ for (const auto& test_case : kTestCases) {
+ std::unique_ptr<TestConversionHost> host =
+ TestConversionHost::ReplaceAndGetConversionHost(web_contents());
+
+ // Secure hosts must be served from the https server.
+ net::EmbeddedTestServer* page_server = (test_case.page_host == kSecureHost)
+ ? https_server()
+ : embedded_test_server();
+ EXPECT_TRUE(NavigateToURL(
+ shell(), page_server->GetURL(test_case.page_host,
+ "/page_with_conversion_redirect.html")));
+
+ net::EmbeddedTestServer* redirect_server =
+ (test_case.redirect_host == kSecureHost) ? https_server()
+ : embedded_test_server();
+ GURL redirect_url = redirect_server->GetURL(
+ test_case.redirect_host,
+ "/server-redirect?" + kWellKnownUrl + "?conversion-data=200");
+ ResourceLoadObserver load_observer(shell());
+ EXPECT_TRUE(ExecJs(web_contents(),
+ JsReplace("createTrackingPixel($1);", redirect_url)));
+
+ // Either wait for a conversion redirect to be received, or wait for the url
+ // to finish loading if we are not expecting a conversions. Because
+ // conversion redirects are blocked, we do not receive completed load
+ // information for them.
+ if (test_case.expected_conversion) {
+ EXPECT_EQ(200UL, host->WaitForNumConversions(1));
+ } else {
+ load_observer.WaitForResourceCompletion(redirect_url);
+ }
+
+ // Navigate the page. By the time the navigation finishes, we will have
+ // received any conversion mojo messages.
+ EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+ EXPECT_EQ(test_case.expected_conversion, host->num_conversions());
+ }
+}
+
+} // namespace content
diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc
index 8e592a6..14a57bf2 100644
--- a/content/browser/storage_partition_impl.cc
+++ b/content/browser/storage_partition_impl.cc
@@ -1697,6 +1697,11 @@
return native_file_system_manager_.get();
}
+ConversionManager* StoragePartitionImpl::GetConversionManager() {
+ DCHECK(initialized_);
+ return conversion_manager_.get();
+}
+
ContentIndexContextImpl* StoragePartitionImpl::GetContentIndexContext() {
DCHECK(initialized_);
return content_index_context_.get();
diff --git a/content/browser/storage_partition_impl.h b/content/browser/storage_partition_impl.h
index 078cc37..6cf0151 100644
--- a/content/browser/storage_partition_impl.h
+++ b/content/browser/storage_partition_impl.h
@@ -191,6 +191,7 @@
PrefetchURLLoaderService* GetPrefetchURLLoaderService();
CookieStoreContext* GetCookieStoreContext();
NativeFileSystemManagerImpl* GetNativeFileSystemManager();
+ ConversionManager* GetConversionManager();
// blink::mojom::StoragePartitionService interface.
void OpenLocalStorage(
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 61519dfd..975a63c 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -48,6 +48,7 @@
#include "content/browser/browser_plugin/browser_plugin_embedder.h"
#include "content/browser/browser_plugin/browser_plugin_guest.h"
#include "content/browser/child_process_security_policy_impl.h"
+#include "content/browser/conversions/conversion_host.h"
#include "content/browser/devtools/protocol/page_handler.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/display_cutout/display_cutout_host_impl.h"
@@ -601,6 +602,10 @@
#if defined(OS_ANDROID)
display_cutout_host_impl_ = std::make_unique<DisplayCutoutHostImpl>(this);
#endif
+
+ // ConversionHost takes a weak ref on |this|, so it must be created outside of
+ // the initializer list.
+ conversion_host_ = std::make_unique<ConversionHost>(this);
}
WebContentsImpl::~WebContentsImpl() {
@@ -1223,6 +1228,11 @@
return it->second;
}
+void WebContentsImpl::RemoveReceiverSetForTesting(
+ const std::string& interface_name) {
+ RemoveReceiverSet(interface_name);
+}
+
std::vector<WebContentsImpl*> WebContentsImpl::GetWebContentsAndAllInner() {
std::vector<WebContentsImpl*> all_contents(1, this);
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 6078c2d..63ad3ce 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -91,6 +91,7 @@
enum class PictureInPictureResult;
class BrowserPluginEmbedder;
class BrowserPluginGuest;
+class ConversionHost;
class DisplayCutoutHostImpl;
class FindRequestManager;
class InterstitialPageImpl;
@@ -280,6 +281,9 @@
// interface.
WebContentsReceiverSet* GetReceiverSet(const std::string& interface_name);
+ // Removes a WebContentsReceiverSet so that it can be overridden for testing.
+ void RemoveReceiverSetForTesting(const std::string& interface_name);
+
// Returns the focused WebContents.
// If there are multiple inner/outer WebContents (when embedding <webview>,
// <guestview>, ...) returns the single one containing the currently focused
@@ -1875,6 +1879,9 @@
// Manages media players, CDMs, and power save blockers for media.
std::unique_ptr<MediaWebContentsObserver> media_web_contents_observer_;
+ // Observes registration of conversions.
+ std::unique_ptr<ConversionHost> conversion_host_;
+
#if BUILDFLAG(ENABLE_PLUGINS)
// Observes pepper playback changes, and notifies MediaSession.
std::unique_ptr<PepperPlaybackObserver> pepper_playback_observer_;
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index b9123ff..9c4ef41 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -332,35 +332,37 @@
// function and using feature string name with EnableFeatureFromString.
const RuntimeFeatureToChromiumFeatureMap<const char*>
runtimeFeatureNameToChromiumFeatureMapping[] = {
- {"FontSrcLocalMatching", features::kFontSrcLocalMatching,
- kUseFeatureState},
- {"LegacyWindowsDWriteFontFallback",
- features::kLegacyWindowsDWriteFontFallback, kUseFeatureState},
{"AddressSpace", network::features::kBlockNonSecureExternalRequests,
kEnableOnly},
- {"BlockCredentialedSubresources",
- features::kBlockCredentialedSubresources, kDisableOnly},
{"AllowContentInitiatedDataUrlNavigations",
features::kAllowContentInitiatedDataUrlNavigations,
kUseFeatureState},
- {"LayoutNG", blink::features::kLayoutNG, kUseFeatureState},
- {"UserAgentClientHint", features::kUserAgentClientHint, kEnableOnly},
{"AudioWorkletRealtimeThread",
blink::features::kAudioWorkletRealtimeThread, kEnableOnly},
- {"TrustedDOMTypes", features::kTrustedDOMTypes, kEnableOnly},
- {"IgnoreCrossOriginWindowWhenNamedAccessOnWindow",
- blink::features::kIgnoreCrossOriginWindowWhenNamedAccessOnWindow,
- kEnableOnly},
- {"StorageAccessAPI", blink::features::kStorageAccessAPI, kEnableOnly},
- {"ShadowDOMV0", blink::features::kWebComponentsV0Enabled,
- kEnableOnly},
- {"CustomElementsV0", blink::features::kWebComponentsV0Enabled,
- kEnableOnly},
- {"HTMLImports", blink::features::kWebComponentsV0Enabled,
+ {"BlockCredentialedSubresources",
+ features::kBlockCredentialedSubresources, kDisableOnly},
+ {"ConversionMeasurement", features::kConversionMeasurement,
kEnableOnly},
{"CSSReducedFontLoadingInvalidations",
blink::features::kCSSReducedFontLoadingInvalidations,
kUseFeatureState},
+ {"CustomElementsV0", blink::features::kWebComponentsV0Enabled,
+ kEnableOnly},
+ {"FontSrcLocalMatching", features::kFontSrcLocalMatching,
+ kUseFeatureState},
+ {"HTMLImports", blink::features::kWebComponentsV0Enabled,
+ kEnableOnly},
+ {"IgnoreCrossOriginWindowWhenNamedAccessOnWindow",
+ blink::features::kIgnoreCrossOriginWindowWhenNamedAccessOnWindow,
+ kEnableOnly},
+ {"LayoutNG", blink::features::kLayoutNG, kUseFeatureState},
+ {"LegacyWindowsDWriteFontFallback",
+ features::kLegacyWindowsDWriteFontFallback, kUseFeatureState},
+ {"ShadowDOMV0", blink::features::kWebComponentsV0Enabled,
+ kEnableOnly},
+ {"StorageAccessAPI", blink::features::kStorageAccessAPI, kEnableOnly},
+ {"TrustedDOMTypes", features::kTrustedDOMTypes, kEnableOnly},
+ {"UserAgentClientHint", features::kUserAgentClientHint, kEnableOnly},
};
for (const auto& mapping : runtimeFeatureNameToChromiumFeatureMapping) {
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 0aea7da4..32c1d90 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -847,6 +847,7 @@
"../browser/child_process_security_policy_browsertest.cc",
"../browser/content_index/content_index_browsertest.cc",
"../browser/content_service_browsertest.cc",
+ "../browser/conversions/conversion_registration_browsertest.cc",
"../browser/cross_origin_opener_policy_browsertest.cc",
"../browser/cross_site_transfer_browsertest.cc",
"../browser/data_decoder_browsertest.cc",
@@ -1563,6 +1564,7 @@
"../browser/code_cache/generated_code_cache_unittest.cc",
"../browser/content_index/content_index_database_unittest.cc",
"../browser/content_index/content_index_service_impl_unittest.cc",
+ "../browser/conversions/conversion_host_unittest.cc",
"../browser/conversions/conversion_policy_unittest.cc",
"../browser/conversions/conversion_storage_sql_unittest.cc",
"../browser/conversions/conversion_storage_unittest.cc",
diff --git a/content/test/data/conversions/page_with_conversion_redirect.html b/content/test/data/conversions/page_with_conversion_redirect.html
new file mode 100644
index 0000000..fde54c3a
--- /dev/null
+++ b/content/test/data/conversions/page_with_conversion_redirect.html
@@ -0,0 +1,4 @@
+<html>
+ <script src="register_conversion.js"></script>
+ This page has a script which creates images that redirect to the conversion registration endpoint.
+</html>
diff --git a/content/test/data/conversions/page_with_preload_conversion_ping.html b/content/test/data/conversions/page_with_preload_conversion_ping.html
new file mode 100644
index 0000000..69c7dd7
--- /dev/null
+++ b/content/test/data/conversions/page_with_preload_conversion_ping.html
@@ -0,0 +1,5 @@
+<html>
+ <head>
+ <link rel="preload" href="server-redirect?.well-known/register-conversion?conversion-data=1" as="image">
+ </head>
+</html>
diff --git a/content/test/data/conversions/page_with_subframe_conversion.html b/content/test/data/conversions/page_with_subframe_conversion.html
new file mode 100644
index 0000000..dad1068
--- /dev/null
+++ b/content/test/data/conversions/page_with_subframe_conversion.html
@@ -0,0 +1,4 @@
+<html>
+ <script src="register_conversion.js"></script>
+ <iframe src="page_with_conversion_redirect.html"></iframe>
+</html>
diff --git a/content/test/data/conversions/register_conversion.js b/content/test/data/conversions/register_conversion.js
new file mode 100644
index 0000000..90246b4
--- /dev/null
+++ b/content/test/data/conversions/register_conversion.js
@@ -0,0 +1,17 @@
+// Copyright 2020 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.
+
+function registerConversion(data) {
+ let img = document.createElement("img");
+ img.src =
+ "server-redirect?.well-known/register-conversion?conversion-data=" +
+ data;
+ document.body.appendChild(img);
+}
+
+function createTrackingPixel(url) {
+ let img = document.createElement("img");
+ img.src = url;
+ document.body.appendChild(img);
+}
diff --git a/third_party/blink/public/mojom/BUILD.gn b/third_party/blink/public/mojom/BUILD.gn
index 65f296c..690dac0 100644
--- a/third_party/blink/public/mojom/BUILD.gn
+++ b/third_party/blink/public/mojom/BUILD.gn
@@ -35,6 +35,7 @@
"commit_result/commit_result.mojom",
"contacts/contacts_manager.mojom",
"content_index/content_index.mojom",
+ "conversions/conversions.mojom",
"cookie_store/cookie_store.mojom",
"crash/crash_memory_metrics_reporter.mojom",
"credentialmanager/credential_manager.mojom",
diff --git a/third_party/blink/public/mojom/conversions/OWNERS b/third_party/blink/public/mojom/conversions/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/third_party/blink/public/mojom/conversions/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/third_party/blink/public/mojom/conversions/conversions.mojom b/third_party/blink/public/mojom/conversions/conversions.mojom
new file mode 100644
index 0000000..2182910a6
--- /dev/null
+++ b/third_party/blink/public/mojom/conversions/conversions.mojom
@@ -0,0 +1,25 @@
+// Copyright 2020 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.
+
+module blink.mojom;
+
+import "url/mojom/origin.mojom";
+
+struct Conversion {
+ // Origin of the conversion registration redirect.
+ url.mojom.Origin reporting_origin;
+
+ // Conversion data specified in conversion redirect. 0 is used as a
+ // default if none is provided.
+ uint64 conversion_data = 0;
+};
+
+// Sent from renderer to browser process when a resource request matching the
+// .well-known conversion registration path is intercepted.
+interface ConversionHost {
+ // Registers a conversion on the site with data provided in the conversion
+ // registration redirect. Only called for requests loaded in the top-level
+ // browsing context.
+ RegisterConversion(Conversion conversion);
+};
\ No newline at end of file
diff --git a/third_party/blink/renderer/core/loader/base_fetch_context.cc b/third_party/blink/renderer/core/loader/base_fetch_context.cc
index 64341a44..92bd7d39 100644
--- a/third_party/blink/renderer/core/loader/base_fetch_context.cc
+++ b/third_party/blink/renderer/core/loader/base_fetch_context.cc
@@ -55,6 +55,13 @@
filter->IsAdResource(request.Url(), request.GetRequestContext()));
}
+bool BaseFetchContext::SendConversionRequestInsteadOfRedirecting(
+ const KURL& url,
+ ResourceRequest::RedirectStatus redirect_status,
+ SecurityViolationReportingPolicy reporting_policy) const {
+ return false;
+}
+
void BaseFetchContext::PrintAccessDeniedMessage(const KURL& url) const {
if (url.IsNull())
return;
@@ -234,6 +241,11 @@
return ResourceRequestBlockedReason::kOther;
}
+ if (SendConversionRequestInsteadOfRedirecting(url, redirect_status,
+ reporting_policy)) {
+ return ResourceRequestBlockedReason::kOther;
+ }
+
// Let the client have the final say into whether or not the load should
// proceed.
if (GetSubresourceFilter() && type != ResourceType::kImportResource) {
diff --git a/third_party/blink/renderer/core/loader/base_fetch_context.h b/third_party/blink/renderer/core/loader/base_fetch_context.h
index 69605437..4ccce5e 100644
--- a/third_party/blink/renderer/core/loader/base_fetch_context.h
+++ b/third_party/blink/renderer/core/loader/base_fetch_context.h
@@ -69,6 +69,14 @@
bool CalculateIfAdSubresource(const ResourceRequest& resource_request,
ResourceType type) override;
+ // Returns whether a request to |url| is a conversion registration request.
+ // Conversion registration requests are redirects to a well-known conversion
+ // registration endpoint.
+ virtual bool SendConversionRequestInsteadOfRedirecting(
+ const KURL& url,
+ ResourceRequest::RedirectStatus redirect_status,
+ SecurityViolationReportingPolicy reporting_policy) const;
+
virtual const ContentSecurityPolicy* GetContentSecurityPolicy() const = 0;
protected:
diff --git a/third_party/blink/renderer/core/loader/frame_fetch_context.cc b/third_party/blink/renderer/core/loader/frame_fetch_context.cc
index 82bbc38..3d19824 100644
--- a/third_party/blink/renderer/core/loader/frame_fetch_context.cc
+++ b/third_party/blink/renderer/core/loader/frame_fetch_context.cc
@@ -36,8 +36,11 @@
#include "base/feature_list.h"
#include "base/optional.h"
#include "build/build_config.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/client_hints/client_hints.h"
#include "third_party/blink/public/common/device_memory/approximated_device_memory.h"
+#include "third_party/blink/public/mojom/conversions/conversions.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h"
@@ -87,6 +90,7 @@
#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/core/timing/performance.h"
#include "third_party/blink/renderer/core/timing/window_performance.h"
+#include "third_party/blink/renderer/core/url/url_search_params.h"
#include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h"
#include "third_party/blink/renderer/platform/exported/wrapped_resource_request.h"
#include "third_party/blink/renderer/platform/instrumentation/histogram.h"
@@ -1057,6 +1061,67 @@
resource_request, type, known_ad);
}
+bool FrameFetchContext::SendConversionRequestInsteadOfRedirecting(
+ const KURL& url,
+ ResourceRequest::RedirectStatus redirect_status,
+ SecurityViolationReportingPolicy reporting_policy) const {
+ if (!RuntimeEnabledFeatures::ConversionMeasurementEnabled())
+ return false;
+
+ // Only register conversions pings that are redirects in the main frame.
+ // TODO(https://crbug.com/1042919): This should also validate that the
+ // redirect is same origin to ensure that the reporting domain has consented
+ // to the registration event.
+ if (!frame_or_imported_document_ || !GetFrame() ||
+ !GetFrame()->IsMainFrame() ||
+ redirect_status != ResourceRequest::RedirectStatus::kFollowedRedirect) {
+ return false;
+ }
+
+ const char kWellKnownConversionRegsitrationPath[] =
+ "/.well-known/register-conversion";
+ if (url.GetPath() != kWellKnownConversionRegsitrationPath)
+ return false;
+
+ // Only allow conversion registration on secure pages with a secure conversion
+ // redirect.
+ scoped_refptr<const SecurityOrigin> redirect_origin =
+ SecurityOrigin::Create(url);
+ if (!GetFrame()
+ ->GetSecurityContext()
+ ->GetSecurityOrigin()
+ ->IsPotentiallyTrustworthy() ||
+ !redirect_origin->IsPotentiallyTrustworthy()) {
+ return false;
+ }
+
+ // Only report conversions for requests with reporting enabled (i.e. do not
+ // count preload requests). However, return true.
+ if (reporting_policy == SecurityViolationReportingPolicy::kSuppressReporting)
+ return true;
+
+ mojom::blink::ConversionPtr conversion = mojom::blink::Conversion::New();
+ conversion->reporting_origin = SecurityOrigin::Create(url);
+ conversion->conversion_data = 0UL;
+
+ const char kConversionDataParam[] = "conversion-data";
+ URLSearchParams* search_params = URLSearchParams::Create(url.Query());
+ if (search_params->has(kConversionDataParam)) {
+ bool is_valid_integer = false;
+ uint64_t data = search_params->get(kConversionDataParam)
+ .ToUInt64Strict(&is_valid_integer);
+
+ // Default invalid params to 0.
+ conversion->conversion_data = is_valid_integer ? data : 0UL;
+ }
+
+ mojo::AssociatedRemote<mojom::blink::ConversionHost> conversion_host;
+ GetFrame()->GetRemoteNavigationAssociatedInterfaces()->GetInterface(
+ &conversion_host);
+ conversion_host->RegisterConversion(std::move(conversion));
+ return true;
+}
+
mojo::PendingReceiver<mojom::blink::WorkerTimingContainer>
FrameFetchContext::TakePendingWorkerTimingReceiver(int request_id) {
return MasterDocumentLoader()->TakePendingWorkerTimingReceiver(request_id);
diff --git a/third_party/blink/renderer/core/loader/frame_fetch_context.h b/third_party/blink/renderer/core/loader/frame_fetch_context.h
index a50534a..280ece0 100644
--- a/third_party/blink/renderer/core/loader/frame_fetch_context.h
+++ b/third_party/blink/renderer/core/loader/frame_fetch_context.h
@@ -109,6 +109,11 @@
bool CalculateIfAdSubresource(const ResourceRequest& resource_request,
ResourceType type) override;
+ bool SendConversionRequestInsteadOfRedirecting(
+ const KURL& url,
+ ResourceRequest::RedirectStatus redirect_status,
+ SecurityViolationReportingPolicy reporting_policy) const override;
+
mojo::PendingReceiver<mojom::blink::WorkerTimingContainer>
TakePendingWorkerTimingReceiver(int request_id) override;
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 6bc3a10..9ab39550 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -369,6 +369,10 @@
status: "experimental",
},
{
+ name: "ConversionMeasurement",
+ status: "test",
+ },
+ {
name: "CookieDeprecationMessages",
status: "experimental",
},