blob: f2e2d5f7ed2b9978920ee4043f4a47021a395d52 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/network_request_manager.h"
#include <optional>
#include <string>
#include <utility>
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/optional_ref.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/webid/flags.h"
#include "content/public/browser/connection_allowlist_util.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/weak_document_ptr.h"
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/schemeful_site.h"
#include "net/base/url_util.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
namespace content::webid {
using DownloadCallback = NetworkRequestManager::DownloadCallback;
using ParseJsonCallback = NetworkRequestManager::ParseJsonCallback;
namespace {
// Host prefix prepended to the eTLD+1 to form the FedCM well-known host:
// "web-identity.well-known.<eTLD+1>".
constexpr char kWebIdentitySubdomainHostPrefix[] = "web-identity.well-known.";
// 1 MiB is an arbitrary upper bound that should account for any reasonable
// response size that is a part of this protocol.
constexpr int maxResponseSizeInKiB = 1024;
ParseStatus GetResponseError(base::optional_ref<std::string> response_body,
int response_code,
const std::string& mime_type) {
if (response_code == net::ERR_NETWORK_ACCESS_REVOKED) {
return ParseStatus::kBlockedByConnectionAllowlist;
}
if (response_code == net::HTTP_NOT_FOUND) {
return ParseStatus::kHttpNotFoundError;
}
if (!response_body) {
return ParseStatus::kNoResponseError;
}
if (!blink::IsJSONMimeType(mime_type)) {
return ParseStatus::kInvalidContentTypeError;
}
return ParseStatus::kSuccess;
}
void OnDownloadedJson(ParseJsonCallback parse_json_callback,
std::optional<std::string> response_body,
int response_code,
const std::string& mime_type,
bool cors_error) {
ParseStatus parse_status =
GetResponseError(response_body, response_code, mime_type);
if (parse_status != ParseStatus::kSuccess) {
std::move(parse_json_callback)
.Run({parse_status, response_code, cors_error}, std::nullopt);
return;
}
std::optional<base::DictValue> value =
base::JSONReader::ReadDict(*response_body, base::JSON_PARSE_RFC);
std::move(parse_json_callback)
.Run({value ? ParseStatus::kSuccess : ParseStatus::kInvalidResponseError,
response_code, cors_error},
std::move(value));
}
} // namespace
GURL ExtractEndpoint(const GURL& provider,
const base::DictValue& response,
const char* key) {
const std::string* endpoint = response.FindString(key);
if (!endpoint || endpoint->empty()) {
return GURL();
}
return provider.Resolve(*endpoint);
}
std::optional<GURL> ComputeWellKnownUrl(const GURL& provider,
const std::string& path) {
GURL well_known_url;
if (net::IsLocalhost(provider) || IsPreservePortsForTestingEnabled()) {
well_known_url = provider.GetWithEmptyPath();
} else {
std::string etld_plus_one = GetDomainAndRegistry(
provider, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (etld_plus_one.empty()) {
return std::nullopt;
}
well_known_url = GURL(provider.GetScheme() + "://" + etld_plus_one);
}
GURL::Replacements replacements;
replacements.SetPathStr(path);
return well_known_url.ReplaceComponents(replacements);
}
std::optional<GURL> ComputeWebIdentitySubdomainWellKnownUrl(
const GURL& provider,
const std::string& path) {
// The subdomain form is only meaningful for hosts that have a registrable
// domain. Localhost / test ports / IP literals continue to use the apex
// (legacy) URL.
if (net::IsLocalhost(provider) || provider.HostIsIPAddress() ||
IsPreservePortsForTestingEnabled()) {
return std::nullopt;
}
GURL site_url = net::SchemefulSite(provider).GetURL();
if (!site_url.is_valid() || site_url.host().empty()) {
return std::nullopt;
}
GURL::Replacements replacements;
std::string subdomain_host =
base::StrCat({kWebIdentitySubdomainHostPrefix, site_url.host()});
replacements.SetHostStr(subdomain_host);
replacements.SetPathStr(path);
return site_url.ReplaceComponents(replacements);
}
NetworkRequestManager::NetworkRequestManager(
const url::Origin& relying_party_origin,
scoped_refptr<network::SharedURLLoaderFactory> loader_factory,
network::mojom::ClientSecurityStatePtr client_security_state,
network::mojom::RequestDestination destination,
FrameTreeNodeId frame_tree_node_id,
WeakDocumentPtr initiator_document)
: relying_party_origin_(relying_party_origin),
loader_factory_(loader_factory),
client_security_state_(std::move(client_security_state)),
destination_(destination),
frame_tree_node_id_(frame_tree_node_id),
initiator_document_(std::move(initiator_document)) {}
NetworkRequestManager::~NetworkRequestManager() = default;
void NetworkRequestManager::DownloadJsonAndParse(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> post_data,
ParseJsonCallback parse_json_callback,
bool allow_http_error_results,
const std::string& content_type) {
DownloadUrl(std::move(resource_request), std::move(post_data),
base::BindOnce(&OnDownloadedJson, std::move(parse_json_callback)),
/*max_download_size=*/maxResponseSizeInKiB * 1024,
allow_http_error_results, content_type);
}
void NetworkRequestManager::DownloadUrl(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> post_data,
DownloadCallback callback,
size_t max_download_size,
bool allow_http_error_results,
const std::string& content_type) {
RenderFrameHost* render_frame_host =
initiator_document_.AsRenderFrameHostIfValid();
if (!render_frame_host) {
// If the initiator frame of the invoking API has gone (e.g. user closes the
// tab). The request is aborted. This is because:
// 1. The connection allowlist is stored in the initiator frame's policy
// container. Since the frame has been destroyed, there is no connection
// allowlist to check.
// 2. It is safe to abort the requests because they are no longer useful
// as the initiator frame owning the API that requires the requests has
// been destroyed.
if (callback) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&NetworkRequestManager::OnRequestBlocked,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback), net::ERR_ABORTED));
}
return;
}
if (post_data) {
resource_request->method = net::HttpRequestHeaders::kPostMethod;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
content_type);
}
// Prepare DevTools instrumentation for the request upfront.
auto request_id = base::UnguessableToken::Create();
devtools_instrumentation::MaybeAssignResourceRequestId(
frame_tree_node_id_, request_id.ToString(), *resource_request);
if (resource_request->devtools_request_id.has_value()) {
devtools_instrumentation::WillSendFedCmNetworkRequest(
frame_tree_node_id_, *resource_request, post_data);
}
if (!content::FrameConnectionAllowlistAllowsRequestAndReportIfNeeded(
render_frame_host, resource_request->url,
/*is_redirect=*/false)) {
// The request URL is not allowed by the initiator frame's connection
// allowlist. See: https://github.com/WICG/connection-allowlists.
if (resource_request->devtools_request_id.has_value()) {
// Notify the dev tools that the request is blocked with network error:
// `net::ERR_NETWORK_ACCESS_REVOKED`.
devtools_instrumentation::DidReceiveFedCmNetworkResponse(
frame_tree_node_id_, request_id.ToString(), resource_request->url,
/*response_head=*/nullptr, /*response_body=*/"",
network::URLLoaderCompletionStatus(net::ERR_NETWORK_ACCESS_REVOKED));
}
if (callback) {
// Run the download callback asynchronously.
//
// The callback must not be run synchronously. This is because
// `ConfigFetcher::Start()` initiates multiple network requests in a loop
// iterating over its member `fetch_results_`.
// If the download callback is run synchronously here:
// 1. The download callback, which is the completion handler (either
// `OnWellKnownFetched` or `OnConfigFetched`), runs immediately.
// 2. Assume this is the last request which is a config request and it is
// blocked by the connection allowlist. In `OnConfigFetched`,
// `ConfigFetcher::RunCallbackIfDone()` will be called and triggers
// `AccountsFetcher::OnAllConfigAndWellKnownFetched()`.
// 3. That function deletes the `ConfigFetcher` object because there are
// no pending requests.
// 4. When the stack unwinds back to `ConfigFetcher::Start()`, the loop
// tries to continue but the `ConfigFetcher` has already been deleted.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&NetworkRequestManager::OnRequestBlocked,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
net::ERR_NETWORK_ACCESS_REVOKED));
}
return;
}
network::ResourceRequest* resource_request_ptr = resource_request.get();
std::unique_ptr<network::SimpleURLLoader> url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
CreateTrafficAnnotation());
network::SimpleURLLoader* url_loader_ptr = url_loader.get();
if (resource_request_ptr->devtools_request_id.has_value()) {
urlloader_devtools_request_id_map_[url_loader_ptr] = request_id;
}
if (post_data) {
url_loader->AttachStringForUpload(*post_data, content_type);
if (allow_http_error_results) {
url_loader->SetAllowHttpErrorResults(true);
}
}
// Callback is a member of NetworkRequestManager in order to cancel callback
// if NetworkRequestManager object is destroyed prior to callback being run.
url_loader_ptr->DownloadToString(
loader_factory_.get(),
base::BindOnce(&NetworkRequestManager::OnDownloadedUrl,
weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
std::move(callback)),
max_download_size);
}
void NetworkRequestManager::OnDownloadedUrl(
std::unique_ptr<network::SimpleURLLoader> url_loader,
DownloadCallback callback,
std::optional<std::string> response_body) {
auto* response_info = url_loader->ResponseInfo();
// Use the HTTP response code, if available. If it is not available, use the
// NetError(). Note that it is acceptable to put these in the same int because
// NetErrors are not positive, so they do not conflict with HTTP error codes.
int response_code = response_info && response_info->headers
? response_info->headers->response_code()
: url_loader->NetError();
std::optional<network::URLLoaderCompletionStatus> status =
url_loader->CompletionStatus();
// Notify DevTools about the response
auto it = urlloader_devtools_request_id_map_.find(url_loader.get());
if (it != urlloader_devtools_request_id_map_.end()) {
auto request_id = it->second;
const std::string& response_body_str =
response_body.value_or(std::string());
auto completion_status = status.value_or(
network::URLLoaderCompletionStatus(url_loader->NetError()));
devtools_instrumentation::DidReceiveFedCmNetworkResponse(
frame_tree_node_id_, request_id.ToString(), url_loader->GetFinalURL(),
response_info, response_body_str, completion_status);
// Remove the entry from the map
urlloader_devtools_request_id_map_.erase(it);
}
if (!callback) {
// For the metrics endpoint, we do not care about the result.
return;
}
std::string mime_type;
if (response_info && response_info->headers) {
response_info->headers->GetMimeType(&mime_type);
}
// Check for CORS error
bool cors_error = false;
if (status && status.value().cors_error_status.has_value()) {
cors_error = true;
}
std::move(callback).Run(std::move(response_body), response_code,
std::move(mime_type), cors_error);
}
void NetworkRequestManager::OnRequestBlocked(DownloadCallback callback,
int response_code) {
if (callback) {
std::move(callback).Run(/*response_body=*/std::nullopt, response_code,
/*mime_type=*/"", /*cors_error=*/false);
}
}
std::unique_ptr<network::ResourceRequest>
NetworkRequestManager::CreateUncredentialedResourceRequest(
const GURL& target_url,
bool send_origin,
bool follow_redirects) const {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = target_url;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
"application/json");
resource_request->destination = destination_;
// See https://github.com/fedidcg/FedCM/issues/379 for why the Origin header
// is sent instead of the Referrer header.
if (send_origin) {
resource_request->headers.SetHeader(net::HttpRequestHeaders::kOrigin,
relying_party_origin_.Serialize());
CHECK(!follow_redirects, base::NotFatalUntil::M158);
}
if (follow_redirects &&
content::FrameConnectionAllowlistAllowsRequestAndReportIfNeeded(
initiator_document_.AsRenderFrameHostIfValid(), resource_request->url,
/*is_redirect=*/true)) {
// Only follow redirects if the initiator frame's connection allowlist
// allows redirects. Otherwise, set the `redirect_mode` to `kError` so that
// redirects will not be followed.
// TODO(crbug.com/482728970): The connection allowlist check on redirect for
// `NetworkRequestManager` is implemented in a different way compared with
// the usual approach used for most other network requests. Instead of
// checking the connection allowlists redirect directive when the redirect
// takes place, `NetworkRequestManager` decides whether the resource request
// follows the redirect during request initiation. This is because the
// request will use the URLLoaderFactory associated with the browser
// process, which makes it difficult to retrieve the connection allowlist
// during the redirect. For connection allowlists reporting, special
// handling may be required.
resource_request->redirect_mode = network::mojom::RedirectMode::kFollow;
} else {
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
}
resource_request->request_initiator = url::Origin();
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther,
/*top_frame_origin=*/relying_party_origin_,
/*frame_origin=*/url::Origin::Create(target_url), net::SiteForCookies(),
/*nonce=*/std::nullopt,
net::NetworkIsolationPartition::kFedCmUncredentialedRequests);
CHECK(client_security_state_, base::NotFatalUntil::M158);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
return resource_request;
}
std::unique_ptr<network::ResourceRequest>
NetworkRequestManager::CreateCredentialedResourceRequest(
const GURL& target_url,
CredentialedResourceRequestType type) const {
auto resource_request = std::make_unique<network::ResourceRequest>();
auto target_origin = url::Origin::Create(target_url);
auto site_for_cookies = net::SiteForCookies::FromOrigin(target_origin);
// Setting the initiator to relying_party_origin_ ensures that we don't send
// SameSite=Strict cookies.
resource_request->request_initiator = relying_party_origin_;
resource_request->destination = destination_;
resource_request->url = target_url;
resource_request->site_for_cookies = site_for_cookies;
// TODO(crbug.com/40284123): Figure out why when using CORS we still need to
// explicitly pass the Origin header.
if (type != CredentialedResourceRequestType::kNoOrigin) {
resource_request->headers.SetHeader(net::HttpRequestHeaders::kOrigin,
relying_party_origin_.Serialize());
}
if (type == CredentialedResourceRequestType::kOriginWithCORS) {
resource_request->mode = network::mojom::RequestMode::kCors;
resource_request->request_initiator = relying_party_origin_;
}
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
"application/json");
resource_request->credentials_mode =
network::mojom::CredentialsMode::kInclude;
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
net::IsolationInfo::RequestType request_type =
net::IsolationInfo::RequestType::kOther;
if (IsSameSiteLaxEnabled()) {
// We use kMainFrame so that we can send SameSite=Lax cookies.
request_type = net::IsolationInfo::RequestType::kMainFrame;
}
resource_request->trusted_params->isolation_info = net::IsolationInfo::Create(
request_type, /*top_frame_origin=*/target_origin,
/*frame_origin=*/target_origin, site_for_cookies);
CHECK(client_security_state_, base::NotFatalUntil::M158);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
return resource_request;
}
} // namespace content::webid