blob: f020d80da3a7aa93a60b4cfbb2df2e4d58bd7e9d [file] [log] [blame]
// Copyright 2021 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/services/auction_worklet/auction_downloader.h"
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "url/gurl.h"
namespace auction_worklet {
namespace {
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("auction_downloader", R"(
semantics {
sender: "AuctionDownloader"
description:
"Requests FLEDGE script or JSON file for running an ad auction."
trigger:
"Requested when running a FLEDGE auction."
data: "URL associated with an interest group or seller."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"These requests cannot be disabled."
policy_exception_justification:
"These requests triggered by a website."
})");
const char kWebAssemblyMime[] = "application/wasm";
// Returns the MIME type string to send for the Accept header for `mime_type`.
// These are the official IANA MIME type strings, though other MIME type strings
// are allows in the response.
base::StringPiece MimeTypeToString(AuctionDownloader::MimeType mime_type) {
switch (mime_type) {
case AuctionDownloader::MimeType::kJavascript:
return base::StringPiece("application/javascript");
case AuctionDownloader::MimeType::kJson:
return base::StringPiece("application/json");
case AuctionDownloader::MimeType::kWebAssembly:
return base::StringPiece(kWebAssemblyMime);
}
}
// Checks if `response_info` is consistent with `mime_type`.
bool MimeTypeIsConsistent(
AuctionDownloader::MimeType mime_type,
const network::mojom::URLResponseHead* response_info) {
switch (mime_type) {
case AuctionDownloader::MimeType::kJavascript:
// ResponseInfo's `mime_type` is always lowercase.
return blink::IsSupportedJavascriptMimeType(response_info->mime_type);
case AuctionDownloader::MimeType::kJson:
// ResponseInfo's `mime_type` is always lowercase.
return blink::IsJSONMimeType(response_info->mime_type);
case AuctionDownloader::MimeType::kWebAssembly: {
std::string raw_content_type;
// Here we use the headers directly, not the parsed mimetype, since we
// much check there are no parameters whatsoever. Ref.
// https://webassembly.github.io/spec/web-api/#streaming-modules
if (!response_info->headers->GetNormalizedHeader(
net::HttpRequestHeaders::kContentType, &raw_content_type)) {
return false;
}
return base::ToLowerASCII(raw_content_type) == kWebAssemblyMime;
}
}
}
// Checks if `charset` is a valid charset, in lowercase ASCII. Takes `body` as
// well, to ensure it uses the specified charset.
bool IsAllowedCharset(base::StringPiece charset, const std::string& body) {
if (charset == "utf-8" || charset.empty()) {
return base::IsStringUTF8(body);
} else if (charset == "us-ascii") {
return base::IsStringASCII(body);
}
// TODO(mmenke): Worth supporting iso-8859-1, or full character set list?
return false;
}
} // namespace
AuctionDownloader::AuctionDownloader(
network::mojom::URLLoaderFactory* url_loader_factory,
const GURL& source_url,
MimeType mime_type,
AuctionDownloaderCallback auction_downloader_callback)
: source_url_(source_url),
mime_type_(mime_type),
auction_downloader_callback_(std::move(auction_downloader_callback)) {
DCHECK(auction_downloader_callback_);
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = source_url;
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
MimeTypeToString(mime_type_));
simple_url_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), kTrafficAnnotation);
// Abort on redirects.
// TODO(mmenke): May want a browser-side proxy to block redirects instead.
simple_url_loader_->SetOnRedirectCallback(base::BindRepeating(
&AuctionDownloader::OnRedirect, base::Unretained(this)));
simple_url_loader_->SetTimeoutDuration(base::Seconds(30));
// TODO(mmenke): Consider limiting the size of response bodies.
simple_url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory, base::BindOnce(&AuctionDownloader::OnBodyReceived,
base::Unretained(this)));
}
AuctionDownloader::~AuctionDownloader() = default;
void AuctionDownloader::OnBodyReceived(std::unique_ptr<std::string> body) {
DCHECK(auction_downloader_callback_);
auto simple_url_loader = std::move(simple_url_loader_);
std::string allow_fledge;
if (!body) {
std::string error_msg;
if (simple_url_loader->ResponseInfo() &&
simple_url_loader->ResponseInfo()->headers &&
simple_url_loader->ResponseInfo()->headers->response_code() / 100 !=
2) {
int status = simple_url_loader->ResponseInfo()->headers->response_code();
error_msg = base::StringPrintf(
"Failed to load %s HTTP status = %d %s.", source_url_.spec().c_str(),
status,
simple_url_loader->ResponseInfo()->headers->GetStatusText().c_str());
} else {
error_msg = base::StringPrintf(
"Failed to load %s error = %s.", source_url_.spec().c_str(),
net::ErrorToString(simple_url_loader->NetError()).c_str());
}
std::move(auction_downloader_callback_)
.Run(nullptr /* body */, nullptr /* headers */, error_msg);
} else if (!simple_url_loader->ResponseInfo()->headers ||
!simple_url_loader->ResponseInfo()->headers->GetNormalizedHeader(
"X-Allow-FLEDGE", &allow_fledge) ||
!base::EqualsCaseInsensitiveASCII(allow_fledge, "true")) {
std::move(auction_downloader_callback_)
.Run(nullptr /* body */, nullptr /* headers */,
base::StringPrintf(
"Rejecting load of %s due to lack of X-Allow-FLEDGE: true.",
source_url_.spec().c_str()));
} else if (!MimeTypeIsConsistent(mime_type_,
simple_url_loader->ResponseInfo())) {
std::move(auction_downloader_callback_)
.Run(nullptr /* body */, nullptr /* headers */,
base::StringPrintf(
"Rejecting load of %s due to unexpected MIME type.",
source_url_.spec().c_str()));
} else if ((mime_type_ != MimeType::kWebAssembly) &&
!IsAllowedCharset(simple_url_loader->ResponseInfo()->charset,
*body)) {
std::move(auction_downloader_callback_)
.Run(nullptr /* body */, nullptr /* headers */,
base::StringPrintf(
"Rejecting load of %s due to unexpected charset.",
source_url_.spec().c_str()));
} else {
// All OK!
std::move(auction_downloader_callback_)
.Run(std::move(body),
std::move(simple_url_loader->ResponseInfo()->headers),
absl::nullopt /* error_msg */);
}
}
void AuctionDownloader::OnRedirect(
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* removed_headers) {
DCHECK(auction_downloader_callback_);
// Need to cancel the load, to prevent the request from continuing.
simple_url_loader_.reset();
std::move(auction_downloader_callback_)
.Run(nullptr /* body */, nullptr /* headers */,
base::StringPrintf("Unexpected redirect on %s.",
source_url_.spec().c_str()));
}
} // namespace auction_worklet