blob: 22600139823885a380094b33a5bd00b1e17ed474 [file] [log] [blame]
// 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 "base/strings/string_util.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/webid/flags.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/data_decoder.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 {
namespace {
constexpr char kApplicationJson[] = "application/json";
// Body content types.
constexpr char kUrlEncodedContentType[] = "application/x-www-form-urlencoded";
// 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(std::string* response_body,
int response_code,
const std::string& mime_type) {
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;
}
ParseStatus GetParsingError(
const data_decoder::DataDecoder::ValueOrError& result) {
if (!result.has_value()) {
return ParseStatus::kInvalidResponseError;
}
return result->GetIfDict() ? ParseStatus::kSuccess
: ParseStatus::kInvalidResponseError;
}
void OnJsonParsed(ParseJsonCallback parse_json_callback,
int response_code,
bool cors_error,
data_decoder::DataDecoder::ValueOrError result) {
ParseStatus parse_status = GetParsingError(result);
std::move(parse_json_callback)
.Run({parse_status, response_code, cors_error}, std::move(result));
}
void OnDownloadedJson(ParseJsonCallback parse_json_callback,
std::unique_ptr<std::string> response_body,
int response_code,
const std::string& mime_type,
bool cors_error) {
ParseStatus parse_status =
GetResponseError(response_body.get(), response_code, mime_type);
if (parse_status != ParseStatus::kSuccess) {
std::move(parse_json_callback)
.Run({parse_status, response_code, cors_error},
data_decoder::DataDecoder::ValueOrError());
return;
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response_body,
base::BindOnce(&OnJsonParsed, std::move(parse_json_callback),
response_code, cors_error));
}
} // namespace
GURL ExtractEndpoint(const GURL& provider,
const base::Value::Dict& 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);
}
NetworkRequestManager::NetworkRequestManager(
const url::Origin& relying_party_origin,
scoped_refptr<network::SharedURLLoaderFactory> loader_factory,
network::mojom::ClientSecurityStatePtr client_security_state,
content::FrameTreeNodeId frame_tree_node_id)
: relying_party_origin_(relying_party_origin),
loader_factory_(loader_factory),
client_security_state_(std::move(client_security_state)),
frame_tree_node_id_(frame_tree_node_id) {}
NetworkRequestManager::~NetworkRequestManager() = default;
void NetworkRequestManager::DownloadJsonAndParse(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> url_encoded_post_data,
ParseJsonCallback parse_json_callback,
bool allow_http_error_results) {
DownloadUrl(std::move(resource_request), std::move(url_encoded_post_data),
base::BindOnce(&OnDownloadedJson, std::move(parse_json_callback)),
/*max_download_size=*/maxResponseSizeInKiB * 1024,
allow_http_error_results);
}
void NetworkRequestManager::DownloadUrl(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> url_encoded_post_data,
DownloadCallback callback,
size_t max_download_size,
bool allow_http_error_results) {
if (url_encoded_post_data) {
resource_request->method = net::HttpRequestHeaders::kPostMethod;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
kUrlEncodedContentType);
}
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();
// Notify DevTools about the request
auto request_id = base::UnguessableToken::Create();
devtools_instrumentation::MaybeAssignResourceRequestId(
frame_tree_node_id_, request_id.ToString(), *resource_request_ptr);
if (resource_request_ptr->devtools_request_id.has_value()) {
urlloader_devtools_request_id_map_[url_loader_ptr] = request_id;
devtools_instrumentation::WillSendFedCmNetworkRequest(
frame_tree_node_id_, *resource_request_ptr, url_encoded_post_data);
}
if (url_encoded_post_data) {
url_loader->AttachStringForUpload(*url_encoded_post_data,
kUrlEncodedContentType);
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::unique_ptr<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 ? *response_body : 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);
}
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,
kApplicationJson);
resource_request->destination =
network::mojom::RequestDestination::kWebIdentity;
// 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());
DCHECK(!follow_redirects);
}
if (follow_redirects) {
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);
DCHECK(client_security_state_);
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 =
network::mojom::RequestDestination::kWebIdentity;
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,
kApplicationJson);
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 (webid::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);
DCHECK(client_security_state_);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
return resource_request;
}
} // namespace content::webid