Skip to content

Commit

Permalink
[Payments] HTTP HEAD request for payment method manifest.
Browse files Browse the repository at this point in the history
Before this patch, Chrome would download payment method manifests via
HTTP GET, which would check the HTTP headers for a Link
rel="payment-method-manifest" first and then, failing to find it, would
fallback to reading the body of the HTTP response. This HTTP GET had a
1MB file limit.

The HTTP GET was problematic for web developers who used the payment
method identifier URL to host both human-readable content and the API
end-point HTTP Link rel="payment-method-manifest" header. If the
human-readable content grew above 1MB, then Chrome rejected the API
end-point, even though it did not need to download the full
human-readable content.

This patch changes Chrome to download the payment method manifest via
HTTP HEAD request on the payment method identifier URL. If the HTTP
headers do not have a Link rel="payment-method-manifest", then Chrome
will fall back to HTTP GET request on this URL.

After this patch, Chrome is more resilient against changes to
human-readable content that is hosted on the same URL as the payment
method identifier API end-point with HTTP header Link
rel="payment-method-identifier".

Bug: 1450596
Change-Id: I26975138546b35a6c77142ffe1f3f4b64df1d833
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4579537
Commit-Queue: Rouslan Solomakhin <rouslan@chromium.org>
Reviewed-by: Nick Burris <nburris@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1154861}
  • Loading branch information
rsolomakhin authored and Chromium LUCI CQ committed Jun 8, 2023
1 parent 3687976 commit faf93ef
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 43 deletions.
74 changes: 55 additions & 19 deletions components/payments/core/payment_manifest_downloader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
Expand Down Expand Up @@ -142,7 +143,7 @@ void PaymentManifestDownloader::DownloadPaymentMethodManifest(
// Restrict number of redirects for efficiency and breaking circle.
InitiateDownload(merchant_origin, url, /*url_before_redirects=*/url,
/*did_follow_redirect=*/false,
Download::Type::RESPONSE_BODY_OR_LINK_HEADER,
Download::Type::LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY,
/*allowed_number_of_redirects=*/3, std::move(callback));
}

Expand Down Expand Up @@ -170,6 +171,14 @@ PaymentManifestDownloader::Download::Download() = default;

PaymentManifestDownloader::Download::~Download() = default;

bool PaymentManifestDownloader::Download::IsLinkHeaderDownload() const {
return type == Type::LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY;
}

bool PaymentManifestDownloader::Download::IsResponseBodyDownload() const {
return type == Type::FALLBACK_TO_RESPONSE_BODY || type == Type::RESPONSE_BODY;
}

void PaymentManifestDownloader::OnURLLoaderRedirect(
network::SimpleURLLoader* url_loader,
const GURL& url_before_redirect,
Expand All @@ -185,7 +194,7 @@ void PaymentManifestDownloader::OnURLLoaderRedirect(
// Manually follow some type of redirects.
std::string error_message;
if (download->allowed_number_of_redirects > 0) {
DCHECK_EQ(Download::Type::RESPONSE_BODY_OR_LINK_HEADER, download->type);
DCHECK(download->IsLinkHeaderDownload());
GURL redirect_url = ParseRedirectUrl(redirect_info, download->original_url,
*log_, &error_message);
if (!redirect_url.is_empty()) {
Expand All @@ -198,7 +207,7 @@ void PaymentManifestDownloader::OnURLLoaderRedirect(
download->request_initiator, redirect_url,
/*url_before_redirects=*/download->url_before_redirects,
/*did_follow_redirect=*/true,
Download::Type::RESPONSE_BODY_OR_LINK_HEADER,
Download::Type::LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY,
--download->allowed_number_of_redirects,
std::move(download->callback));
return;
Expand Down Expand Up @@ -258,26 +267,27 @@ void PaymentManifestDownloader::OnURLLoaderCompleteInternal(
}

std::string error_message;
if (download->type == Download::Type::RESPONSE_BODY) {
if (!headers) {
RespondWithError(errors::kPaymentManifestDownloadFailed, final_url, *log_,
std::move(download->callback));
} else if (headers->response_code() != net::HTTP_OK) {
if (download->IsResponseBodyDownload()) {
if (headers && headers->response_code() != net::HTTP_OK) {
RespondWithHttpStatusCodeError(
final_url, static_cast<net::HttpStatusCode>(headers->response_code()),
*log_, std::move(download->callback));
} else {
RespondWithContent(response_body, errors::kNoContentInPaymentManifest,
final_url, *log_, std::move(download->callback));
RespondWithContent(
response_body,
download->type == Download::Type::FALLBACK_TO_RESPONSE_BODY
? errors::kNoContentAndNoLinkHeader
: errors::kNoContentInPaymentManifest,
final_url, *log_, std::move(download->callback));
}
return;
}

DCHECK_EQ(Download::Type::RESPONSE_BODY_OR_LINK_HEADER, download->type);
DCHECK(download->IsLinkHeaderDownload());

if (!headers) {
RespondWithContent(response_body, errors::kNoContentAndNoLinkHeader,
final_url, *log_, std::move(download->callback));
// Fallback to HTTP GET when HTTP HEAD response has no headers.
FallbackToDownloadingResponseBody(final_url, std::move(download));
return;
}

Expand All @@ -292,8 +302,9 @@ void PaymentManifestDownloader::OnURLLoaderCompleteInternal(
std::string link_header;
headers->GetNormalizedHeader("link", &link_header);
if (link_header.empty()) {
RespondWithContent(response_body, errors::kNoContentAndNoLinkHeader,
final_url, *log_, std::move(download->callback));
// Fallback to HTTP GET when HTTP HEAD response does not contain a Link
// header.
FallbackToDownloadingResponseBody(final_url, std::move(download));
return;
}

Expand Down Expand Up @@ -345,8 +356,22 @@ void PaymentManifestDownloader::OnURLLoaderCompleteInternal(
}
}

RespondWithContent(response_body, errors::kNoContentAndNoLinkHeader,
final_url, *log_, std::move(download->callback));
// Fallback to HTTP GET when HTTP HEAD response does not contain a Link header
// with rel="payment-method-manifest".
FallbackToDownloadingResponseBody(final_url, std::move(download));
}

void PaymentManifestDownloader::FallbackToDownloadingResponseBody(
const GURL& url_to_download,
std::unique_ptr<Download> download_info) {
InitiateDownload(
/*request_initiator=*/download_info->request_initiator,
/*url=*/url_to_download,
/*url_before_redirects=*/download_info->url_before_redirects,
/*did_follow_redirect=*/download_info->did_follow_redirect,
/*download_type=*/Download::Type::FALLBACK_TO_RESPONSE_BODY,
/*allowed_number_of_redirects=*/0,
/*callback=*/std::move(download_info->callback));
}

network::SimpleURLLoader* PaymentManifestDownloader::GetLoaderForTesting() {
Expand All @@ -372,7 +397,8 @@ void PaymentManifestDownloader::InitiateDownload(
// Only initial download of the payment method manifest (which might contain
// an HTTP Link header) is allowed to redirect.
DCHECK(allowed_number_of_redirects == 0 ||
download_type == Download::Type::RESPONSE_BODY_OR_LINK_HEADER);
download_type ==
Download::Type::LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY);

net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("payment_manifest_downloader", R"(
Expand All @@ -397,7 +423,17 @@ void PaymentManifestDownloader::InitiateDownload(
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->request_initiator = request_initiator;
resource_request->url = url;
resource_request->method = "GET";

switch (download_type) {
case Download::Type::LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY:
resource_request->method = net::HttpRequestHeaders::kHeadMethod;
break;
case Download::Type::FALLBACK_TO_RESPONSE_BODY:
// Intentional fall through.
case Download::Type::RESPONSE_BODY:
resource_request->method = net::HttpRequestHeaders::kGetMethod;
break;
}
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
Expand Down
15 changes: 14 additions & 1 deletion components/payments/core/payment_manifest_downloader.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,22 @@ class PaymentManifestDownloader {
// Information about an ongoing download request.
struct Download {
enum class Type {
RESPONSE_BODY_OR_LINK_HEADER,
LINK_HEADER_WITH_FALLBACK_TO_RESPONSE_BODY,
FALLBACK_TO_RESPONSE_BODY,
RESPONSE_BODY,
};

Download();
~Download();

// Returns true if this download is an HTTP HEAD request for a payment
// manifest.
bool IsLinkHeaderDownload() const;

// Returns true if this download is an HTTP GET request either for payment
// method manifest or for a web app manifest file.
bool IsResponseBodyDownload() const;

int allowed_number_of_redirects = 0;
Type type = Type::RESPONSE_BODY;
url::Origin request_initiator;
Expand Down Expand Up @@ -175,6 +184,10 @@ class PaymentManifestDownloader {
scoped_refptr<net::HttpResponseHeaders> headers,
int net_error);

void FallbackToDownloadingResponseBody(
const GURL& url_to_download,
std::unique_ptr<Download> download_info);

// Called by unittests to get the one in-progress loader.
network::SimpleURLLoader* GetLoaderForTesting();

Expand Down
79 changes: 56 additions & 23 deletions components/payments/core/payment_manifest_downloader_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ class PaymentMethodManifestDownloaderTest
base::BindOnce(&PaymentMethodManifestDownloaderTest::OnManifestDownload,
base::Unretained(this)));
}

// Simulates two responses for payment method manifest download:
// 1) Only HTTP header without the response body content.
// 2) Both HTTP header and the response body content.
void ServerHeaderAndFallbackResponse(int response_code,
Headers send_headers,
absl::optional<std::string> link_header,
const std::string& response_body,
int net_error) {
ServerResponse(response_code, send_headers, link_header, kNoResponseBody,
net_error);
ServerResponse(response_code, send_headers, link_header, response_body,
net_error);
}
};

TEST_F(PaymentMethodManifestDownloaderTest, FirstHttpResponse404IsFailure) {
Expand All @@ -146,14 +160,16 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" "
"HTTP header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kOmit, kNoLinkHeader, kNoResponseBody, net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kOmit, kNoLinkHeader,
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
NoHttpHeadersButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response body", kNoError));

ServerResponse(200, Headers::kOmit, kNoLinkHeader, "response body", net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kOmit, kNoLinkHeader,
"response body", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
Expand All @@ -164,15 +180,16 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" HTTP "
"header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kSend, kNoLinkHeader, kNoResponseBody, net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, kNoLinkHeader,
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
EmptyHttpHeaderButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response content", kNoError));

ServerResponse(200, Headers::kSend, kNoLinkHeader, "response content",
net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, kNoLinkHeader,
"response content", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
Expand All @@ -183,16 +200,16 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" "
"HTTP header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kSend, kEmptyLinkHeader, kNoResponseBody,
net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, kEmptyLinkHeader,
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
EmptyHttpLinkHeaderButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response body", kNoError));

ServerResponse(200, Headers::kSend, kEmptyLinkHeader, "response body",
net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, kEmptyLinkHeader,
"response body", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
Expand All @@ -203,16 +220,16 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" "
"HTTP header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kSend, "<manifest.json>", kNoResponseBody,
net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, "<manifest.json>",
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
NoRelInHttpLinkHeaderButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response body", kNoError));

ServerResponse(200, Headers::kSend, "<manifest.json>", "response body",
net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend, "<manifest.json>",
"response body", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
Expand All @@ -223,16 +240,18 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" "
"HTTP header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kSend, "rel=payment-method-manifest",
kNoResponseBody, net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend,
"rel=payment-method-manifest",
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
NoUrlInHttpLinkHeaderButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response body", kNoError));

ServerResponse(200, Headers::kSend, "rel=payment-method-manifest",
"response body", net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend,
"rel=payment-method-manifest",
"response body", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
Expand All @@ -243,16 +262,18 @@ TEST_F(PaymentMethodManifestDownloaderTest,
"No content and no \"Link: rel=payment-method-manifest\" "
"HTTP header found at \"https://bobpay.test/\"."));

ServerResponse(200, Headers::kSend, "<manifest.json>; rel=web-app-manifest",
kNoResponseBody, net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend,
"<manifest.json>; rel=web-app-manifest",
kNoResponseBody, net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
NoManifestRellInHttpLinkHeaderButWithResponseBodyIsSuccess) {
EXPECT_CALL(*this, OnManifestDownload(_, "response body", kNoError));

ServerResponse(200, Headers::kSend, "<manifest.json>; rel=web-app-manifest",
"response body", net::OK);
ServerHeaderAndFallbackResponse(200, Headers::kSend,
"<manifest.json>; rel=web-app-manifest",
"response body", net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest, SecondHttpResponse404IsFailure) {
Expand Down Expand Up @@ -283,14 +304,26 @@ TEST_F(PaymentMethodManifestDownloaderTest, EmptySecondResponseIsFailure) {
}

TEST_F(PaymentMethodManifestDownloaderTest,
SecondResponseWithoutHeadersIsFailure) {
SecondResponseWithoutHeadersButWithContentIsSuccess) {
ServerResponse(200, Headers::kSend,
"<manifest.json>; rel=payment-method-manifest",
kNoResponseBody, net::OK);

EXPECT_CALL(*this, OnManifestDownload(_, "response content", kNoError));

ServerResponse(200, Headers::kOmit, kNoLinkHeader, "response content",
net::OK);
}

TEST_F(PaymentMethodManifestDownloaderTest,
SecondResponseWithoutContentIsFailure) {
ServerResponse(200, Headers::kSend,
"<manifest.json>; rel=payment-method-manifest",
kNoResponseBody, net::OK);

EXPECT_CALL(*this,
OnManifestDownload(_, kNoContent,
"Unable to download payment manifest "
"No content found in payment manifest "
"\"https://bobpay.test/manifest.json\"."));

ServerResponse(200, Headers::kOmit, kNoLinkHeader, kNoResponseBody, net::OK);
Expand Down

0 comments on commit faf93ef

Please sign in to comment.