blob: b7a3fe769064bbd8a71b5212b82cd3162407d506 [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 "services/network/url_loader_util.h"
#include <algorithm>
#include <optional>
#include "base/containers/enum_set.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "net/base/elements_upload_data_stream.h"
#include "net/base/upload_bytes_element_reader.h"
#include "net/base/upload_data_stream.h"
#include "net/base/upload_file_element_reader.h"
#include "net/cookies/cookie_util.h"
#include "net/http/http_connection_info.h"
#include "net/http/http_response_info.h"
#include "net/storage_access_api/status.h"
#include "net/url_request/url_request.h"
#include "services/network/ad_heuristic_cookie_overrides.h"
#include "services/network/attribution/attribution_request_helper.h"
#include "services/network/chunked_data_pipe_upload_data_stream.h"
#include "services/network/cookie_manager.h"
#include "services/network/cookie_settings.h"
#include "services/network/data_pipe_element_reader.h"
#include "services/network/network_context.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/parsed_headers.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/cpp/sri_message_signatures.h"
#include "services/network/public/cpp/unencoded_digests.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/network_context.mojom-shared.h"
#include "services/network/public/mojom/url_request.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/sec_header_helpers.h"
#include "services/network/shared_resource_checker.h"
namespace network::url_loader_util {
namespace {
// TODO(https://crbug.com/375352611): add the check for enabling third-party
// cookies.
constexpr uint64_t kAllowedDevToolsCookieSettingOverrides =
1u << static_cast<int>(
net::CookieSettingOverride::kForceDisableThirdPartyCookies) |
1u << static_cast<int>(
net::CookieSettingOverride::kForceEnableThirdPartyCookieMitigations) |
1u << static_cast<int>(net::CookieSettingOverride::kSkipTPCDMetadataGrant) |
1u << static_cast<int>(
net::CookieSettingOverride::kSkipTPCDHeuristicsGrant);
bool IsMultiplexedConnection(const net::HttpResponseInfo& response_info) {
switch (net::HttpConnectionInfoToCoarse(response_info.connection_info)) {
case net::HttpConnectionInfoCoarse::kHTTP1:
return false;
case net::HttpConnectionInfoCoarse::kHTTP2:
case net::HttpConnectionInfoCoarse::kQUIC:
return true;
case net::HttpConnectionInfoCoarse::kOTHER:
return false;
}
}
const char* GetDestinationTypePartString(
network::mojom::RequestDestination destination) {
if (destination == network::mojom::RequestDestination::kDocument) {
return "MainFrame";
} else if (destination == network::mojom::RequestDestination::kFrame ||
destination == network::mojom::RequestDestination::kIframe) {
return "SubFrame";
}
return "Subresource";
}
const char* GetCertStatePartString(const net::SSLInfo& ssl_info) {
if (!ssl_info.cert.get()) {
return "NoCert";
}
return ssl_info.is_issued_by_known_root ? "KnownRootCert" : "UnknownRootCert";
}
// Returns true if the |credentials_mode| of the request allows sending
// credentials.
bool ShouldAllowCredentials(mojom::CredentialsMode credentials_mode) {
switch (credentials_mode) {
case mojom::CredentialsMode::kInclude:
// TODO(crbug.com/40619226): Make this work with
// CredentialsMode::kSameOrigin.
case mojom::CredentialsMode::kSameOrigin:
return true;
case mojom::CredentialsMode::kOmit:
case mojom::CredentialsMode::kOmitBug_775438_Workaround:
return false;
}
}
// Returns true when the |credentials_mode| of the request allows sending client
// certificates.
bool ShouldSendClientCertificates(mojom::CredentialsMode credentials_mode) {
switch (credentials_mode) {
case mojom::CredentialsMode::kInclude:
case mojom::CredentialsMode::kSameOrigin:
return true;
// TODO(crbug.com/40089326): Due to a bug, the default behavior does
// not properly correspond to Fetch's "credentials mode", in that client
// certificates will be sent if available, or the handshake will be aborted
// to allow selecting a client cert.
// With the feature kOmitCorsClientCert enabled, the correct
// behavior is done; omit all client certs and continue the handshake
// without sending one if requested.
case mojom::CredentialsMode::kOmit:
return !base::FeatureList::IsEnabled(features::kOmitCorsClientCert);
case mojom::CredentialsMode::kOmitBug_775438_Workaround:
return false;
}
}
// Returns whether sending/storing credentials is allowed by COEP and
// Document-Isolation-Policy.
// |url| is the latest request URL, either the original URL or
// `redirect_info.new_url`.
// When Cross-Origin-Embedder-Policy: credentialless or
// Document-Isolation-Policy: isolate-and-credentialless are set, do not send
// or store credentials for no-cors cross-origin request.
// [spec]:
// https://fetch.spec.whatwg.org/#cross-origin-embedder-policy-allows-credentials
bool WebPoliciesAllowCredentials(
const GURL& url,
const network::mojom::ClientSecurityStatePtr& client_security_state,
mojom::RequestMode request_mode,
const std::optional<url::Origin>& initiator) {
// [spec]: To check if Cross-Origin-Embedder-Policy allows credentials, given
// a request request, run these steps:
// [spec] 1. If request’s mode is not "no-cors", then return true.
switch (request_mode) {
case mojom::RequestMode::kCors:
case mojom::RequestMode::kCorsWithForcedPreflight:
case mojom::RequestMode::kNavigate:
case mojom::RequestMode::kSameOrigin:
return true;
case mojom::RequestMode::kNoCors:
break;
}
// [spec]: 2. If request’s client is null, then return true.
if (!client_security_state) {
return true;
}
// [spec]: 3. If request’s client’s policy container’s embedder policy’s value
// is not "credentialless", then return true.
// Document-Isolation-Policy: also check that Document-Isolation-Policy allows
// credentials.
if (client_security_state->cross_origin_embedder_policy.value !=
mojom::CrossOriginEmbedderPolicyValue::kCredentialless &&
client_security_state->document_isolation_policy.value !=
mojom::DocumentIsolationPolicyValue::kIsolateAndCredentialless) {
return true;
}
// [spec]: 4. If request’s origin is same origin with request’s current URL’s
// origin and request does not have a redirect-tainted origin, then
// return true.
url::Origin request_initiator = initiator.value_or(url::Origin());
if (request_initiator.IsSameOriginWith(url)) {
return true;
}
// [spec]: 5. Return false.
return false;
}
bool ShouldForceIgnoreSiteForCookies(
const ResourceRequest& request,
const cors::OriginAccessList& origin_access_list) {
// Ignore site for cookies in requests from an initiator covered by the
// same-origin-policy exclusions in `origin_access_list_` (typically requests
// initiated by Chrome Extensions).
if (request.request_initiator.has_value() &&
cors::OriginAccessList::AccessState::kAllowed ==
origin_access_list.CheckAccessState(request.request_initiator.value(),
request.url)) {
return true;
}
// Convert `site_for_cookies` into an origin (an opaque origin if
// `net::SiteForCookies::IsNull()` returns true).
//
// Note that `site_for_cookies` is a _site_ rather than an _origin_, but for
// Chrome Extensions the _site_ and _origin_ of a host are the same extension
// id. Thanks to this, for Chrome Extensions, we can pass a _site_ into
// OriginAccessChecks (which normally expect an _origin_).
url::Origin site_origin =
url::Origin::Create(request.site_for_cookies.RepresentativeUrl());
// If `site_for_cookies` represents an origin that is granted access to the
// initiator and the target by `origin_access_list_` (typically such
// `site_for_cookies` represents a Chrome Extension), then we also should
// force ignoring of site for cookies if the initiator and the target are
// same-site.
//
// Ideally we would walk up the frame tree and check that each ancestor is
// first-party to the main frame (treating the `origin_access_list_`
// exceptions as "first-party"). But walking up the tree is not possible in
// //services/network and so we make do with just checking the direct
// initiator of the request.
//
// We also check same-siteness between the initiator and the requested URL,
// because setting `force_ignore_site_for_cookies` to true causes Strict
// cookies to be attached, and having the initiator be same-site to the
// request URL is a requirement for Strict cookies (see
// net::cookie_util::ComputeSameSiteContext).
if (!site_origin.opaque() && request.request_initiator.has_value()) {
bool site_can_access_target =
cors::OriginAccessList::AccessState::kAllowed ==
origin_access_list.CheckAccessState(site_origin, request.url);
bool site_can_access_initiator =
cors::OriginAccessList::AccessState::kAllowed ==
origin_access_list.CheckAccessState(
site_origin, request.request_initiator->GetURL());
net::SiteForCookies site_of_initiator =
net::SiteForCookies::FromOrigin(request.request_initiator.value());
bool are_initiator_and_target_same_site =
site_of_initiator.IsFirstParty(request.url);
if (site_can_access_initiator && site_can_access_target &&
are_initiator_and_target_same_site) {
return true;
}
}
return false;
}
// A subclass of net::UploadBytesElementReader which owns
// ResourceRequestBody.
class BytesElementReader : public net::UploadBytesElementReader {
public:
BytesElementReader(ResourceRequestBody* resource_request_body,
const DataElementBytes& element)
: net::UploadBytesElementReader(element.bytes()),
resource_request_body_(resource_request_body) {}
BytesElementReader(const BytesElementReader&) = delete;
BytesElementReader& operator=(const BytesElementReader&) = delete;
~BytesElementReader() override = default;
private:
scoped_refptr<ResourceRequestBody> resource_request_body_;
};
// A subclass of net::UploadFileElementReader which owns
// ResourceRequestBody.
// This class is necessary to ensure the BlobData and any attached shareable
// files survive until upload completion.
class FileElementReader : public net::UploadFileElementReader {
public:
FileElementReader(ResourceRequestBody* resource_request_body,
base::TaskRunner* task_runner,
const DataElementFile& element,
base::File&& file)
: net::UploadFileElementReader(task_runner,
std::move(file),
element.path(),
element.offset(),
element.length(),
element.expected_modification_time()),
resource_request_body_(resource_request_body) {}
FileElementReader(const FileElementReader&) = delete;
FileElementReader& operator=(const FileElementReader&) = delete;
~FileElementReader() override = default;
private:
scoped_refptr<ResourceRequestBody> resource_request_body_;
};
} // namespace
// Returns if `request` is fetch upload request with a streaming body.
bool HasFetchStreamingUploadBody(const ResourceRequest& request) {
const ResourceRequestBody* request_body = request.request_body.get();
if (!request_body) {
return false;
}
const std::vector<DataElement>* elements = request_body->elements();
if (elements->size() != 1u) {
return false;
}
const auto& element = elements->front();
return element.type() == mojom::DataElementDataView::Tag::kChunkedDataPipe &&
element.As<network::DataElementChunkedDataPipe>().read_only_once();
}
std::unique_ptr<net::UploadDataStream> CreateUploadDataStream(
ResourceRequestBody* body,
std::vector<base::File>& opened_files,
base::SequencedTaskRunner* file_task_runner) {
// In the case of a chunked upload, there will just be one element.
if (body->elements()->size() == 1) {
if (body->elements()->begin()->type() ==
network::mojom::DataElementDataView::Tag::kChunkedDataPipe) {
auto& element =
body->elements_mutable()->at(0).As<DataElementChunkedDataPipe>();
const bool has_null_source = element.read_only_once().value();
auto upload_data_stream =
std::make_unique<ChunkedDataPipeUploadDataStream>(
body, element.ReleaseChunkedDataPipeGetter(), has_null_source);
if (element.read_only_once()) {
upload_data_stream->EnableCache();
}
return upload_data_stream;
}
}
auto opened_file = opened_files.begin();
std::vector<std::unique_ptr<net::UploadElementReader>> element_readers;
for (const auto& element : *body->elements()) {
switch (element.type()) {
case network::mojom::DataElementDataView::Tag::kBytes:
element_readers.push_back(std::make_unique<BytesElementReader>(
body, element.As<DataElementBytes>()));
break;
case network::mojom::DataElementDataView::Tag::kFile:
CHECK(opened_file != opened_files.end());
element_readers.push_back(std::make_unique<FileElementReader>(
body, file_task_runner, element.As<network::DataElementFile>(),
std::move(*opened_file++)));
break;
case network::mojom::DataElementDataView::Tag::kDataPipe: {
element_readers.push_back(std::make_unique<DataPipeElementReader>(
body,
element.As<network::DataElementDataPipe>().CloneDataPipeGetter()));
break;
}
case network::mojom::DataElementDataView::Tag::kChunkedDataPipe: {
// This shouldn't happen, as the traits logic should ensure that if
// there's a chunked pipe, there's one and only one element.
NOTREACHED();
}
}
}
DCHECK(opened_file == opened_files.end());
return std::make_unique<net::ElementsUploadDataStream>(
std::move(element_readers), body->identifier());
}
net::CookieSettingOverrides CalculateCookieSettingOverrides(
net::CookieSettingOverrides factory_overrides,
net::CookieSettingOverrides devtools_overrides,
const ResourceRequest& request,
bool emit_metrics) {
net::CookieSettingOverrides overrides(factory_overrides);
if (request.is_outermost_main_frame &&
network::cors::IsCorsEnabledRequestMode(request.mode)) {
overrides.Put(
net::CookieSettingOverride::kTopLevelStorageAccessGrantEligible);
}
AddAdsHeuristicCookieSettingOverrides(request.is_ad_tagged, overrides,
emit_metrics);
// Only apply the DevTools overrides if the request is from devtools enabled
// context.
if (request.devtools_request_id.has_value()) {
CHECK_EQ(devtools_overrides.ToEnumBitmask() &
~kAllowedDevToolsCookieSettingOverrides,
0u);
overrides = base::Union(overrides, devtools_overrides);
}
// If `factory_overrides` contains
// `net::CookieSettingOverride::kStorageAccessGrantEligible`, then
// well-behaved clients ensure that `request.storage_access_api_status` is
// `net::StorageAccessApiStatus::kAccessViaAPI`. But since clients may be
// compromised, we do not CHECK that this holds.
//
// Note: `kStorageAccessGrantEligible` and `request.storage_access_api_status`
// are not trusted for security or privacy decisions.
// Add the Storage Access override enum based on whether the request's url and
// initiator are same-site, to prevent cross-site sibling iframes benefit from
// each other's storage access API grants. This must be updated on redirects.
if (net::cookie_util::ShouldAddInitialStorageAccessApiOverride(
request.url, request.storage_access_api_status,
request.request_initiator, emit_metrics,
request.credentials_mode == mojom::CredentialsMode::kInclude)) {
overrides.Put(net::CookieSettingOverride::kStorageAccessGrantEligible);
}
// The `kStorageAccessGrantEligibleViaHeader` override will be applied
// (in-place) by individual request jobs as appropriate, but should not be
// present initially.
CHECK(!overrides.Has(
net::CookieSettingOverride::kStorageAccessGrantEligibleViaHeader));
return overrides;
}
std::optional<net::IsolationInfo> GetIsolationInfo(
const net::IsolationInfo& factory_isolation_info,
bool automatically_assign_isolation_info,
const ResourceRequest& request) {
if (!factory_isolation_info.IsEmpty()) {
return factory_isolation_info;
}
if (request.trusted_params &&
!request.trusted_params->isolation_info.IsEmpty()) {
if (request.credentials_mode != network::mojom::CredentialsMode::kOmit) {
DCHECK(request.trusted_params->isolation_info.site_for_cookies()
.IsEquivalent(request.site_for_cookies));
}
return request.trusted_params->isolation_info;
}
if (automatically_assign_isolation_info) {
url::Origin origin = url::Origin::Create(request.url);
return net::IsolationInfo::Create(net::IsolationInfo::RequestType::kOther,
origin, origin, net::SiteForCookies());
}
return std::nullopt;
}
// Retrieves the Cookie header from either `cors_exempt_headers` or `headers`.
std::string GetCookiesFromHeaders(
const net::HttpRequestHeaders& headers,
const net::HttpRequestHeaders& cors_exempt_headers) {
std::optional<std::string> cookies =
cors_exempt_headers.GetHeader(net::HttpRequestHeaders::kCookie);
if (!cookies) {
cookies = headers.GetHeader(net::HttpRequestHeaders::kCookie);
}
return std::move(cookies).value_or(std::string());
}
void RecordURLLoaderRequestMetrics(const net::URLRequest& url_request,
size_t raw_request_line_size,
size_t raw_request_headers_size) {
// All histograms recorded here are of the form:
// "NetworkService.Requests.{Multiplexed}.{RequestType}.{Method}.{Result}
// .{Metric}".
// For example:
// "NetworkService.Requests.Simple.MainFrame.Get.Success.TotalRequestSize".
absl::InlinedVector<std::string_view, 10> histogram_prefix_pieces = {
"NetworkService", "Requests"};
const net::HttpResponseInfo& response_info = url_request.response_info();
if (IsMultiplexedConnection(response_info)) {
histogram_prefix_pieces.push_back("Multiplexed");
} else {
histogram_prefix_pieces.push_back("Simple");
}
switch (url_request.isolation_info().request_type()) {
case net::IsolationInfo::RequestType::kMainFrame:
histogram_prefix_pieces.push_back("MainFrame");
break;
case net::IsolationInfo::RequestType::kSubFrame:
case net::IsolationInfo::RequestType::kOther:
// TODO(crbug.com/362787712): Add metrics for other types of requests.
return;
}
if (url_request.method() == "GET") {
histogram_prefix_pieces.push_back("Get");
} else {
// Other types of requests need to be handled differently e.g. the total
// request size of a POST request needs to include the body.
// TODO(crbug.com/362787712): Add metrics for other types of requests.
return;
}
const int response_code = response_info.headers->response_code();
if (response_code < 199) {
// Ignore information responses because they are not complete requests.
return;
} else if (response_code < 299 || response_code < 399) {
// We consider redirects a success.
histogram_prefix_pieces.push_back("Success");
} else if (response_code < 499) {
histogram_prefix_pieces.push_back("ClientError");
} else if (response_code < 599) {
histogram_prefix_pieces.push_back("ServerError");
} else {
// Ignore unexpected server response codes.
return;
}
auto make_histogram_name =
[&histogram_prefix_pieces](std::string_view metric) {
histogram_prefix_pieces.push_back(metric);
std::string name = base::JoinString(histogram_prefix_pieces, ".");
histogram_prefix_pieces.pop_back();
return name;
};
base::UmaHistogramCounts100000(make_histogram_name("TotalUrlSize"),
url_request.url().spec().size());
// HTTP/2 and HTTP/3 requests don't separate request line from headers so no
// need to record header metrics separately.
if (!IsMultiplexedConnection(response_info)) {
base::UmaHistogramCounts100000(make_histogram_name("TotalHeadersSize"),
raw_request_headers_size);
}
// For HTTP/2 and HTTP/3 the request line is included in the headers, but
// `raw_request_line_size_` is 0 for these requests, so we can add it
// unconditionally for all requests.
size_t total_request_size = raw_request_headers_size + raw_request_line_size;
base::UmaHistogramCounts100000(make_histogram_name("TotalRequestSize"),
total_request_size);
}
void MaybeRecordSharedDictionaryUsedResponseMetrics(
int error_code,
network::mojom::RequestDestination destination,
const net::HttpResponseInfo& response_info,
bool shared_dictionary_allowed_check_passed) {
if (response_info.was_cached) {
return;
}
if (response_info.did_use_shared_dictionary) {
base::UmaHistogramSparse(
base::StrCat({"Net.SharedDictionaryUsedResponseErrorCodes2.",
GetDestinationTypePartString(destination), ".",
GetCertStatePartString(response_info.ssl_info)}),
-error_code);
}
if (shared_dictionary_allowed_check_passed &&
destination == network::mojom::RequestDestination::kDocument) {
base::UmaHistogramBoolean(
base::StrCat(
{"Net.SharedDictionaryUsedByResponseWhenAvailable2.MainFrame.",
net::HttpConnectionInfoCoarseToString(
net::HttpConnectionInfoToCoarse(
response_info.connection_info)),
".", GetCertStatePartString(response_info.ssl_info)}),
response_info.did_use_shared_dictionary);
}
}
void ConfigureUrlRequest(const ResourceRequest& request,
const mojom::URLLoaderFactoryParams& factory_params,
const cors::OriginAccessList& origin_access_list,
net::URLRequest& url_request,
SharedResourceChecker& shared_resource_checker) {
url_request.set_method(request.method);
url_request.set_site_for_cookies(request.site_for_cookies);
url_request.set_force_ignore_site_for_cookies(
ShouldForceIgnoreSiteForCookies(request, origin_access_list));
if (!request.navigation_redirect_chain.empty()) {
DCHECK_EQ(request.mode, mojom::RequestMode::kNavigate);
url_request.SetURLChain(request.navigation_redirect_chain);
}
url_request.SetReferrer(request.referrer.GetAsReferrer().spec());
url_request.set_referrer_policy(request.referrer_policy);
url_request.set_upgrade_if_insecure(request.upgrade_if_insecure);
url_request.set_ad_tagged(request.is_ad_tagged);
url_request.set_client_side_content_decoding_enabled(
request.client_side_content_decoding_enabled);
auto isolation_info = GetIsolationInfo(
factory_params.isolation_info,
factory_params.automatically_assign_isolation_info, request);
if (isolation_info) {
// set_isolation_info sets the url_request's cookie_partition_key which can
// then be used when checking IsSharedResource().
url_request.set_isolation_info(std::move(isolation_info).value());
url_request.set_is_shared_resource(shared_resource_checker.IsSharedResource(
request, url_request.isolation_info().top_frame_origin(),
url_request.cookie_partition_key()));
}
// When a service worker forwards a navigation request it uses the
// service worker's IsolationInfo. This causes the cookie code to fail
// to send SameSite=Lax cookies for main-frame navigations passed through
// a service worker. To fix this we check to see if the original destination
// of the request was a main frame document and then set a flag indicating
// SameSite cookies should treat it as a main frame navigation.
url_request.set_force_main_frame_for_same_site_cookies(
request.mode == mojom::RequestMode::kNavigate &&
request.destination == mojom::RequestDestination::kEmpty &&
request.original_destination == mojom::RequestDestination::kDocument);
url_request.SetSecureDnsPolicy(
factory_params.disable_secure_dns ||
(request.trusted_params &&
request.trusted_params->disable_secure_dns)
? net::SecureDnsPolicy::kDisable
: net::SecureDnsPolicy::kAllow);
// |cors_exempt_headers| must be merged here to avoid breaking CORS checks.
// They are non-empty when the values are given by the UA code, therefore
// they should be ignored by CORS checks.
net::HttpRequestHeaders merged_headers = request.headers;
merged_headers.MergeFrom(ComputeAttributionReportingHeaders(request));
merged_headers.MergeFrom(request.cors_exempt_headers);
// This should be ensured by the CorsURLLoaderFactory(), which is called
// before URLLoaders are created.
DCHECK(AreRequestHeadersSafe(merged_headers));
url_request.SetExtraRequestHeaders(std::move(merged_headers));
url_request.set_accepted_stream_types(request.devtools_accepted_stream_types);
url_request.set_initiator(request.request_initiator);
// Note: There are some ordering dependencies here. `SetRequestCredentials`
// depends on `SetLoadFlags`; `CalculateStorageAccessStatus` depends on
// `cookie_setting_overrides` and `SetRequestCredentials`.
// `SetFetchMetadataHeaders` depends on
// `url_request.storage_access_status()`.
url_request.cookie_setting_overrides() = CalculateCookieSettingOverrides(
factory_params.cookie_setting_overrides,
factory_params.devtools_cookie_setting_overrides, request,
/*emit_metrics=*/true);
url_request.SetLoadFlags(request.load_flags);
SetRequestCredentials(request.url, factory_params.client_security_state,
request.mode, request.credentials_mode,
request.request_initiator, url_request);
if (request.credentials_mode == mojom::CredentialsMode::kInclude) {
url_request.set_storage_access_status(
url_request.CalculateStorageAccessStatus());
}
SetFetchMetadataHeaders(
url_request, request.mode,
request.trusted_params && request.trusted_params->has_user_activation,
request.destination, /*pending_redirect_url=*/std::nullopt,
factory_params, origin_access_list, request.credentials_mode);
MaybeSetAcceptSignatureHeader(&url_request, request.expected_public_keys);
url_request.set_first_party_url_policy(
request.update_first_party_url_on_redirect
? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
: net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL);
url_request.SetPriorityIncremental(request.priority_incremental);
if (request.socket_tag != net::SocketTag()) {
url_request.set_socket_tag(request.socket_tag);
}
url_request.set_allows_device_bound_session_registration(
request.allows_device_bound_session_registration);
if (base::FeatureList::IsEnabled(features::kSendSameSiteLaxForFedCM) &&
request.destination == mojom::RequestDestination::kWebIdentity) {
// This check is enforced by CorsURLLoaderFactory::IsValidRequest.
CHECK(request.redirect_mode == mojom::RedirectMode::kError ||
request.credentials_mode == mojom::CredentialsMode::kOmit);
url_request.set_ignore_unsafe_method_for_same_site_lax(true);
}
}
void SetRequestCredentials(
const GURL& url,
const network::mojom::ClientSecurityStatePtr& client_security_state,
mojom::RequestMode request_mode,
mojom::CredentialsMode credentials_mode,
const std::optional<url::Origin>& initiator,
net::URLRequest& url_request) {
bool policies_allow_credentials = WebPoliciesAllowCredentials(
url, client_security_state, request_mode, initiator);
bool allow_credentials =
ShouldAllowCredentials(credentials_mode) && policies_allow_credentials;
bool allow_client_certificates =
ShouldSendClientCertificates(credentials_mode) &&
policies_allow_credentials;
// The decision not to include credentials is sticky. This is equivalent to
// checking the tainted origin flag in the fetch specification.
if (!allow_credentials) {
url_request.set_allow_credentials(false);
}
if (!allow_client_certificates) {
url_request.set_send_client_certs(false);
}
// Contrary to Firefox or blink's cache, the HTTP cache doesn't distinguish
// requests including user's credentials from the anonymous ones yet. See
// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U
// As a workaround until a solution is implemented, the cached responses
// aren't used for those requests.
if (!policies_allow_credentials) {
url_request.SetLoadFlags(url_request.load_flags() | net::LOAD_BYPASS_CACHE);
}
}
const mojom::ClientSecurityState* SelectClientSecurityState(
const mojom::ClientSecurityState*
url_loader_factory_params_client_security_state,
const mojom::ClientSecurityState* trusted_params_client_security_state) {
if (trusted_params_client_security_state) {
return trusted_params_client_security_state;
}
return url_loader_factory_params_client_security_state;
}
mojom::URLResponseHeadPtr BuildResponseHead(
const net::URLRequest& url_request,
const net::cookie_util::ParsedRequestCookies& request_cookies,
network::mojom::IPAddressSpace client_address_space,
network::mojom::IPAddressSpace response_address_space,
int32_t url_load_options,
bool load_with_storage_access,
bool is_load_timing_enabled,
bool include_load_timing_internal_info_with_response,
base::TimeTicks response_start,
const raw_ptr<mojom::DevToolsObserver> devtools_observer,
const std::string& devtools_request_id) {
auto response = mojom::URLResponseHead::New();
response->request_time = url_request.request_time();
response->response_time = url_request.response_time();
response->original_response_time = url_request.original_response_time();
response->headers = url_request.response_headers();
response->parsed_headers =
PopulateParsedHeaders(response->headers.get(), url_request.url());
url_request.GetCharset(&response->charset);
response->content_length = url_request.GetExpectedContentSize();
url_request.GetMimeType(&response->mime_type);
const net::HttpResponseInfo& response_info = url_request.response_info();
response->was_fetched_via_spdy = response_info.was_fetched_via_spdy;
response->was_alpn_negotiated = response_info.was_alpn_negotiated;
response->alpn_negotiated_protocol = response_info.alpn_negotiated_protocol;
response->alternate_protocol_usage = response_info.alternate_protocol_usage;
response->connection_info = response_info.connection_info;
response->remote_endpoint = response_info.remote_endpoint;
response->was_fetched_via_cache = url_request.was_cached();
response->is_validated = (response_info.cache_entry_status ==
net::HttpResponseInfo::ENTRY_VALIDATED);
response->proxy_chain = url_request.proxy_chain();
response->network_accessed = response_info.network_accessed;
response->async_revalidation_requested =
response_info.async_revalidation_requested;
response->was_in_prefetch_cache =
!(url_request.load_flags() & net::LOAD_PREFETCH) &&
response_info.unused_since_prefetch;
response->did_use_shared_dictionary = response_info.did_use_shared_dictionary;
response->did_use_server_http_auth = response_info.did_use_server_http_auth;
response->device_bound_session_usage =
static_cast<network::mojom::DeviceBoundSessionUsage>(
url_request.device_bound_session_usage());
// IsInclude() true means the cookie was sent.
response->was_cookie_in_request = std::ranges::any_of(
url_request.maybe_sent_cookies(),
[](const auto& cookie_with_access_result) {
return cookie_with_access_result.access_result.status.IsInclude();
});
if (is_load_timing_enabled) {
url_request.GetLoadTimingInfo(&response->load_timing);
}
if (include_load_timing_internal_info_with_response) {
response->load_timing_internal_info =
url_request.GetLoadTimingInternalInfo();
}
CHECK(include_load_timing_internal_info_with_response ||
!response->load_timing_internal_info);
if (url_request.ssl_info().cert.get()) {
response->cert_status = url_request.ssl_info().cert_status;
if ((url_load_options & mojom::kURLLoadOptionSendSSLInfoWithResponse) ||
(net::IsCertStatusError(url_request.ssl_info().cert_status) &&
(url_load_options &
mojom::kURLLoadOptionSendSSLInfoForCertificateError))) {
response->ssl_info = url_request.ssl_info();
}
}
response->request_cookies = request_cookies;
response->request_start = url_request.creation_time();
response->response_start = response_start;
response->encoded_data_length = url_request.GetTotalReceivedBytes();
response->auth_challenge_info = url_request.auth_challenge_info();
response->has_range_requested = url_request.extra_request_headers().HasHeader(
net::HttpRequestHeaders::kRange);
response->dns_aliases =
base::ToVector(url_request.response_info().dns_aliases);
// [spec]: https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
// 13. Set response’s request-includes-credentials to includeCredentials.
response->request_include_credentials = url_request.allow_credentials();
response->client_address_space = client_address_space;
response->response_address_space = response_address_space;
response->load_with_storage_access = load_with_storage_access;
url_request.GetClientSideContentDecodingTypes(
&response->client_side_content_decoding_types);
if (response->headers) {
response->unencoded_digests =
ParseUnencodedDigestsFromHeaders(*response->headers.get());
ReportUnencodedDigestIssuesToDevtools(
response->unencoded_digests, devtools_observer, devtools_request_id,
url_request.url());
}
return response;
}
} // namespace network::url_loader_util