| // Copyright 2014 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/renderer_host/navigation_request.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/debug/alias.h" |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/safe_ref.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/no_destructor.h" |
| #include "base/not_fatal_until.h" |
| #include "base/notreached.h" |
| #include "base/rand_util.h" |
| #include "base/state_transitions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/types/optional_util.h" |
| #include "base/types/pass_key.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.h" |
| #include "components/viz/host/host_frame_sink_manager.h" |
| #include "content/browser/agent_cluster_key.h" |
| #include "content/browser/blob_storage/chrome_blob_storage_context.h" |
| #include "content/browser/browsing_topics/header_util.h" |
| #include "content/browser/child_process_security_policy_impl.h" |
| #include "content/browser/client_hints/client_hints.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/devtools/network_service_devtools_observer.h" |
| #include "content/browser/download/download_manager_impl.h" |
| #include "content/browser/fenced_frame/fenced_frame_url_mapping.h" |
| #include "content/browser/interest_group/ad_auction_headers_util.h" |
| #include "content/browser/loader/browser_initiated_resource_request.h" |
| #include "content/browser/loader/cached_navigation_url_loader.h" |
| #include "content/browser/loader/navigation_early_hints_manager.h" |
| #include "content/browser/loader/navigation_url_loader.h" |
| #include "content/browser/loader/object_navigation_fallback_body_loader.h" |
| #include "content/browser/loader/url_loader_factory_utils.h" |
| #include "content/browser/navigation_or_document_handle.h" |
| #include "content/browser/network/cross_origin_embedder_policy_reporter.h" |
| #include "content/browser/network_service_instance_impl.h" |
| #include "content/browser/origin_agent_cluster_isolation_state.h" |
| #include "content/browser/origin_trials/origin_trials_utils.h" |
| #include "content/browser/permissions/permission_controller_impl.h" |
| #include "content/browser/preloading/prefetch/prefetch_document_manager.h" |
| #include "content/browser/preloading/prefetch/prefetch_features.h" |
| #include "content/browser/preloading/prefetch/prefetch_serving_page_metrics_container.h" |
| #include "content/browser/preloading/prerender/prerender_host_registry.h" |
| #include "content/browser/preloading/prerender/prerender_metrics.h" |
| #include "content/browser/preloading/prerender/prerender_navigation_utils.h" |
| #include "content/browser/process_lock.h" |
| #include "content/browser/renderer_host/back_forward_cache_impl.h" |
| #include "content/browser/renderer_host/concurrent_navigations_commit_deferring_condition.h" |
| #include "content/browser/renderer_host/cookie_utils.h" |
| #include "content/browser/renderer_host/debug_urls.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_controller_impl.h" |
| #include "content/browser/renderer_host/navigation_request_info.h" |
| #include "content/browser/renderer_host/navigation_state_keep_alive.h" |
| #include "content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_cache.h" |
| #include "content/browser/renderer_host/navigator.h" |
| #include "content/browser/renderer_host/navigator_delegate.h" |
| #include "content/browser/renderer_host/page_delegate.h" |
| #include "content/browser/renderer_host/private_network_access_util.h" |
| #include "content/browser/renderer_host/render_frame_host_csp_context.h" |
| #include "content/browser/renderer_host/render_frame_host_delegate.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/render_frame_proxy_host.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/browser/renderer_host/render_view_host_delegate.h" |
| #include "content/browser/renderer_host/render_view_host_impl.h" |
| #include "content/browser/renderer_host/scoped_view_transition_resources.h" |
| #include "content/browser/renderer_host/subframe_history_navigation_throttle.h" |
| #include "content/browser/renderer_host/system_entropy_utils.h" |
| #include "content/browser/scoped_active_url.h" |
| #include "content/browser/security/coop/cross_origin_opener_policy_reporter.h" |
| #include "content/browser/service_worker/service_worker_client.h" |
| #include "content/browser/service_worker/service_worker_context_wrapper.h" |
| #include "content/browser/service_worker/service_worker_main_resource_handle.h" |
| #include "content/browser/shared_storage/shared_storage_header_observer.h" |
| #include "content/browser/site_info.h" |
| #include "content/browser/site_instance_impl.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/browser/url_loader_factory_params_helper.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache.h" |
| #include "content/common/content_constants_internal.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/common/debug_utils.h" |
| #include "content/common/features.h" |
| #include "content/common/navigation_params_utils.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/child_process_host.h" |
| #include "content/public/browser/client_hints_controller_delegate.h" |
| #include "content/public/browser/commit_deferring_condition.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/cookie_access_details.h" |
| #include "content/public/browser/global_request_id.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_ui_data.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/browser/network_service_util.h" |
| #include "content/public/browser/origin_trials_controller_delegate.h" |
| #include "content/public/browser/peak_gpu_memory_tracker_factory.h" |
| #include "content/public/browser/reduce_accept_language_controller_delegate.h" |
| #include "content/public/browser/reduce_accept_language_utils.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/runtime_feature_state/runtime_feature_state_document_data.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/weak_document_ptr.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/origin_util.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "mojo/public/cpp/system/data_pipe.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/base/url_util.h" |
| #include "net/cookies/cookie_access_result.h" |
| #include "net/filter/source_stream_type.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/storage_access_api/status.h" |
| #include "net/url_request/redirect_info.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/client_hints.h" |
| #include "services/network/public/cpp/content_decoding_interceptor.h" |
| #include "services/network/public/cpp/content_security_policy/content_security_policy.h" |
| #include "services/network/public/cpp/cross_origin_embedder_policy.h" |
| #include "services/network/public/cpp/cross_origin_opener_policy.h" |
| #include "services/network/public/cpp/cross_origin_resource_policy.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/header_util.h" |
| #include "services/network/public/cpp/integrity_policy.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/cpp/permissions_policy/fenced_frame_permissions_policies.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy_features.h" |
| #include "services/network/public/cpp/resource_request_body.h" |
| #include "services/network/public/cpp/supports_loading_mode/supports_loading_mode_parser.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/cpp/web_sandbox_flags.h" |
| #include "services/network/public/mojom/connection_change_observer_client.mojom.h" |
| #include "services/network/public/mojom/device_bound_sessions.mojom.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h" |
| #include "services/network/public/mojom/supports_loading_mode.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom-forward.h" |
| #include "services/network/public/mojom/url_response_head.mojom-shared.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "services/network/public/mojom/web_client_hints_types.mojom-shared.h" |
| #include "services/network/public/mojom/web_client_hints_types.mojom.h" |
| #include "services/network/public/mojom/web_sandbox_flags.mojom.h" |
| #include "third_party/abseil-cpp/absl/cleanup/cleanup.h" |
| #include "third_party/blink/public/common/blob/blob_utils.h" |
| #include "third_party/blink/public/common/chrome_debug_urls.h" |
| #include "third_party/blink/public/common/client_hints/client_hints.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" |
| #include "third_party/blink/public/common/fenced_frame/redacted_fenced_frame_config.h" |
| #include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h" |
| #include "third_party/blink/public/common/frame/frame_owner_element_type.h" |
| #include "third_party/blink/public/common/navigation/navigation_params.h" |
| #include "third_party/blink/public/common/navigation/navigation_params_mojom_traits.h" |
| #include "third_party/blink/public/common/navigation/navigation_policy.h" |
| #include "third_party/blink/public/common/navigation/preloading_headers.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token_validator.h" |
| #include "third_party/blink/public/common/permissions_policy/document_policy.h" |
| #include "third_party/blink/public/common/permissions_policy/policy_helper_public.h" |
| #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h" |
| #include "third_party/blink/public/common/runtime_feature_state/runtime_feature_state_context.h" |
| #include "third_party/blink/public/common/security/address_space_feature.h" |
| #include "third_party/blink/public/common/user_agent/user_agent_metadata.h" |
| #include "third_party/blink/public/common/web_preferences/web_preferences.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h" |
| #include "third_party/blink/public/mojom/loader/mixed_content.mojom.h" |
| #include "third_party/blink/public/mojom/loader/transferrable_url_loader.mojom.h" |
| #include "third_party/blink/public/mojom/navigation/navigation_params.mojom-shared.h" |
| #include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h" |
| #include "third_party/blink/public/mojom/navigation/prefetched_signed_exchange_info.mojom.h" |
| #include "third_party/blink/public/mojom/runtime_feature_state/runtime_feature.mojom.h" |
| #include "third_party/blink/public/mojom/service_worker/service_worker_provider.mojom.h" |
| #include "third_party/blink/public/mojom/storage_key/ancestor_chain_bit.mojom.h" |
| #include "third_party/blink/public/mojom/timing/resource_timing.mojom-forward.h" |
| #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h" |
| #include "third_party/blink/public/platform/resource_request_blocked_reason.h" |
| #include "ui/compositor/compositor_lock.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "ui/android/window_android.h" |
| #include "ui/android/window_android_compositor.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| // Default timeout for the READY_TO_COMMIT -> COMMIT transition. Chosen |
| // initially based on the Navigation.ReadyToCommitUntilCommit UMA, and then |
| // refined based on feedback based on CrashExitCodes.Renderer/RESULT_CODE_HUNG. |
| constexpr base::TimeDelta kDefaultCommitTimeout = base::Seconds(30); |
| |
| // Timeout for the READY_TO_COMMIT -> COMMIT transition. |
| // Overrideable via SetCommitTimeoutForTesting. |
| base::TimeDelta g_commit_timeout = kDefaultCommitTimeout; |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Timeout for locking the compositor at the beginning of navigation. |
| constexpr base::TimeDelta kCompositorLockTimeout = base::Milliseconds(150); |
| #endif |
| |
| const char kSecSharedStorageWritableRequestHeaderKey[] = |
| "Sec-Shared-Storage-Writable"; |
| |
| constexpr char kNavigationRequestScope[] = "NavigationRequestScope"; |
| |
| // Flag to control whether redirect URLs are being sanitized before sending |
| // them to the renderer process as part of the navigation. |
| // See https://crbug.com/40095391. |
| BASE_FEATURE(kSanitizeRedirectUrlsDuringNavigation, |
| "SanitizeRedirectUrlsDuringNavigation", |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| const base::FeatureParam<bool> kDeferSpeculativeRFHWaitUntilFinalResponse{ |
| &features::kDeferSpeculativeRFHCreation, "wait_until_final_response", |
| false}; |
| |
| // Denotes the type of user agent string value sent in the User-Agent request |
| // header. |
| // |
| // Corresponds to the "UserAgentStringType" histogram enumeration type in |
| // tools/metrics/histograms/enums.xml. |
| // |
| // PLEASE DO NOT REORDER, REMOVE, OR CHANGE THE MEANING OF THESE VALUES. |
| enum class UserAgentStringType { |
| kFullVersion, |
| kReducedVersion, |
| kOverriden, |
| kMaxValue = kOverriden |
| }; |
| |
| // Returns the net load flags to use based on the navigation type. |
| // TODO(clamy): Remove the blink code that sets the caching flags. |
| void UpdateLoadFlagsWithCacheFlags(int* load_flags, |
| blink::mojom::NavigationType navigation_type, |
| bool is_post) { |
| switch (navigation_type) { |
| case blink::mojom::NavigationType::RELOAD: |
| *load_flags |= net::LOAD_VALIDATE_CACHE; |
| break; |
| case blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE: |
| *load_flags |= net::LOAD_BYPASS_CACHE; |
| break; |
| case blink::mojom::NavigationType::RESTORE: |
| *load_flags |= net::LOAD_SKIP_CACHE_VALIDATION; |
| break; |
| case blink::mojom::NavigationType::RESTORE_WITH_POST: |
| *load_flags |= |
| net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION; |
| break; |
| case blink::mojom::NavigationType::SAME_DOCUMENT: |
| case blink::mojom::NavigationType::DIFFERENT_DOCUMENT: |
| if (is_post) |
| *load_flags |= net::LOAD_VALIDATE_CACHE; |
| break; |
| case blink::mojom::NavigationType::HISTORY_SAME_DOCUMENT: |
| case blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT: |
| if (is_post) { |
| *load_flags |= |
| net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION; |
| } else { |
| *load_flags |= net::LOAD_SKIP_CACHE_VALIDATION; |
| } |
| break; |
| } |
| } |
| |
| // This should match blink::ResourceRequest::needsHTTPOrigin. |
| bool NeedsHTTPOrigin(net::HttpRequestHeaders* headers, |
| const std::string& method) { |
| // Blink version of this function checks if the Origin header might have |
| // already been added to |headers|. This check is not replicated below |
| // because: |
| // 1. We want to overwrite the old (renderer-provided) header value |
| // with a new, trustworthy (browser-provided) value. |
| // 2. The rest of the function matches the Blink version, so there should |
| // be no discrepancies in the Origin value used. |
| |
| // Don't send an Origin header for GET or HEAD to avoid privacy issues. |
| // For example, if an intranet page has a hyperlink to an external web |
| // site, we don't want to include the Origin of the request because it |
| // will leak the internal host name. Similar privacy concerns have lead |
| // to the widespread suppression of the Referer header at the network |
| // layer. |
| if (method == "GET" || method == "HEAD") |
| return false; |
| |
| // For non-GET and non-HEAD methods, always send an Origin header so the |
| // server knows we support this feature. |
| return true; |
| } |
| |
| // Computes the value that should be set for the User-Agent header, if |
| // `user_agent_override` is non-empty, `user_agent_override` is returned as the |
| // header value. |
| std::string ComputeUserAgentValue(const net::HttpRequestHeaders& headers, |
| const std::string& user_agent_override, |
| content::BrowserContext* context) { |
| if (!user_agent_override.empty()) { |
| base::UmaHistogramEnumeration("Navigation.UserAgentStringType", |
| UserAgentStringType::kOverriden); |
| return user_agent_override; |
| } |
| |
| base::UmaHistogramEnumeration( |
| "Navigation.UserAgentStringType", |
| base::FeatureList::IsEnabled( |
| blink::features::kReduceUserAgentMinorVersion) |
| ? UserAgentStringType::kReducedVersion |
| : UserAgentStringType::kFullVersion); |
| |
| return GetContentClient()->browser()->GetUserAgentBasedOnPolicy(context); |
| } |
| |
| void AddAdditionalRequestHeaders( |
| net::HttpRequestHeaders* headers, |
| const GURL& url, |
| blink::mojom::NavigationType navigation_type, |
| ui::PageTransition transition, |
| BrowserContext* browser_context, |
| const std::string& method, |
| const std::string& user_agent_override, |
| const std::optional<url::Origin>& initiator_origin, |
| blink::mojom::Referrer* referrer, |
| FrameTreeNode* frame_tree_node) { |
| if (!url.SchemeIsHTTPOrHTTPS()) |
| return; |
| |
| bool is_reload = NavigationTypeUtils::IsReload(navigation_type); |
| RenderViewHostImpl* render_view_host = |
| frame_tree_node->current_frame_host()->render_view_host(); |
| const blink::RendererPreferences& render_prefs = |
| render_view_host->GetDelegate()->GetRendererPrefs(render_view_host); |
| UpdateAdditionalHeadersForBrowserInitiatedRequest( |
| headers, browser_context, is_reload, render_prefs, |
| /*is_for_worker_script*=*/false); |
| |
| // Tack an 'Upgrade-Insecure-Requests' header to outgoing navigational |
| // requests, as described in |
| // https://w3c.github.io/webappsec/specs/upgrade/#feature-detect |
| headers->SetHeaderIfMissing("Upgrade-Insecure-Requests", "1"); |
| |
| headers->SetHeaderIfMissing( |
| net::HttpRequestHeaders::kUserAgent, |
| ComputeUserAgentValue(*headers, user_agent_override, browser_context)); |
| |
| if (!render_prefs.enable_referrers) { |
| *referrer = |
| blink::mojom::Referrer(GURL(), network::mojom::ReferrerPolicy::kNever); |
| } |
| |
| // Next, set the HTTP Origin if needed. |
| if (NeedsHTTPOrigin(headers, method)) { |
| url::Origin origin_header_value = initiator_origin.value_or(url::Origin()); |
| origin_header_value = Referrer::SanitizeOriginForRequest( |
| url, origin_header_value, referrer->policy); |
| headers->SetHeader(net::HttpRequestHeaders::kOrigin, |
| origin_header_value.Serialize()); |
| } |
| |
| if (base::FeatureList::IsEnabled(features::kDocumentPolicyNegotiation)) { |
| const blink::DocumentPolicyFeatureState& required_policy = |
| frame_tree_node->effective_frame_policy().required_document_policy; |
| if (!required_policy.empty()) { |
| std::optional<std::string> policy_header = |
| blink::DocumentPolicy::Serialize(required_policy); |
| DCHECK(policy_header); |
| headers->SetHeader("Sec-Required-Document-Policy", policy_header.value()); |
| } |
| } |
| |
| // Add the "Sec-Purpose: prefetch;prerender" header to prerender navigations |
| // including subframe navigations. Add "Purpose: prefetch" as well for |
| // compatibility concerns (See |
| // https://github.com/WICG/nav-speculation/issues/133). |
| if (frame_tree_node->frame_tree().is_prerendering()) { |
| PrerenderHost::GetFromFrameTreeNode(*frame_tree_node) |
| .AddAdditionalRequestHeaders(*headers, *frame_tree_node); |
| } else if (frame_tree_node->frame_tree() |
| .page_delegate() |
| ->IsPageInPreviewMode()) { |
| // Preview mode sends similar request so that it is compatible with |
| // prerendering as we can as possible, but adds `preview` for sites that |
| // need to identify the preview case from prerendering. |
| // Do not send the `Purpose` header as the preview mode is new and don't |
| // need to be careful about the compatibility breakage here. |
| headers->SetHeader(blink::kSecPurposeHeaderName, |
| blink::kSecPurposePrefetchPrerenderPreviewHeaderValue); |
| } |
| } |
| |
| bool ShouldPropagateUserActivation(const url::Origin& previous_origin, |
| const url::Origin& new_origin) { |
| if ((previous_origin.scheme() != "http" && |
| previous_origin.scheme() != "https") || |
| (new_origin.scheme() != "http" && new_origin.scheme() != "https")) { |
| return false; |
| } |
| |
| if (previous_origin.host() == new_origin.host()) |
| return true; |
| |
| std::string previous_domain = |
| net::registry_controlled_domains::GetDomainAndRegistry( |
| previous_origin, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| std::string new_domain = |
| net::registry_controlled_domains::GetDomainAndRegistry( |
| new_origin, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| return !previous_domain.empty() && previous_domain == new_domain; |
| } |
| |
| // LOG_NAVIGATION_TIMING_HISTOGRAM logs |value| for "Navigation.<histogram>" UMA |
| // as well as supplementary UMAs (depending on |transition| and |priority|) |
| // for BackForward/Reload/NewNavigation variants. |
| // |
| // kMaxTime and kBuckets constants are consistent with |
| // UmaHistogramMediumTimes, but a custom kMinTime is used for high |
| // fidelity near the low end of measured values. |
| // |
| // TODO(csharrison,nasko): This macro is incorrect for subframe navigations, |
| // which will only have subframe-specific transition types. This means that all |
| // subframes currently are tagged as NewNavigations. |
| #define LOG_NAVIGATION_TIMING_HISTOGRAM(histogram, transition, priority, \ |
| duration) \ |
| do { \ |
| const base::TimeDelta kMinTime = base::Milliseconds(1); \ |
| const base::TimeDelta kMaxTime = base::Minutes(3); \ |
| const int kBuckets = 50; \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram, duration, kMinTime, \ |
| kMaxTime, kBuckets); \ |
| if (transition & ui::PAGE_TRANSITION_FORWARD_BACK) { \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram ".BackForward", \ |
| duration, kMinTime, kMaxTime, kBuckets); \ |
| } else if (ui::PageTransitionCoreTypeIs(transition, \ |
| ui::PAGE_TRANSITION_RELOAD)) { \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram ".Reload", duration, \ |
| kMinTime, kMaxTime, kBuckets); \ |
| } else if (ui::PageTransitionIsNewNavigation(transition)) { \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram ".NewNavigation", \ |
| duration, kMinTime, kMaxTime, kBuckets); \ |
| } else { \ |
| NOTREACHED() << "Invalid page transition: " << transition; \ |
| } \ |
| if (priority.has_value()) { \ |
| if (priority.value() == base::Process::Priority::kBestEffort) { \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram \ |
| ".BackgroundProcessPriority", \ |
| duration, kMinTime, kMaxTime, kBuckets); \ |
| } else { \ |
| UMA_HISTOGRAM_CUSTOM_TIMES("Navigation." histogram \ |
| ".ForegroundProcessPriority", \ |
| duration, kMinTime, kMaxTime, kBuckets); \ |
| } \ |
| } \ |
| } while (0) |
| |
| void RecordStartToCommitMetrics(base::TimeTicks navigation_start_time, |
| ui::PageTransition transition, |
| const base::TimeTicks& ready_to_commit_time, |
| std::optional<base::Process::Priority> priority, |
| bool is_same_process, |
| bool is_main_frame) { |
| base::TimeTicks now = base::TimeTicks::Now(); |
| base::TimeDelta delta = now - navigation_start_time; |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit", transition, priority, delta); |
| if (is_main_frame) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.MainFrame", transition, |
| priority, delta); |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.Subframe", transition, |
| priority, delta); |
| } |
| if (is_same_process) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.SameProcess", transition, |
| priority, delta); |
| if (is_main_frame) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.SameProcess.MainFrame", |
| transition, priority, delta); |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.SameProcess.Subframe", |
| transition, priority, delta); |
| } |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.CrossProcess", transition, |
| priority, delta); |
| if (is_main_frame) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.CrossProcess.MainFrame", |
| transition, priority, delta); |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("StartToCommit.CrossProcess.Subframe", |
| transition, priority, delta); |
| } |
| } |
| if (!ready_to_commit_time.is_null()) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("ReadyToCommitUntilCommit2", transition, |
| priority, now - ready_to_commit_time); |
| } |
| } |
| |
| void RecordReadyToCommitMetrics( |
| RenderFrameHostImpl* old_rfh, |
| RenderFrameHostImpl* new_rfh, |
| const blink::mojom::CommonNavigationParams& common_params, |
| base::TimeTicks ready_to_commit_time, |
| NavigationRequest::OriginAgentClusterEndResult |
| origin_agent_cluster_end_result, |
| bool did_receive_early_hints_before_cross_origin_redirect) { |
| bool is_main_frame = !new_rfh->GetParent(); |
| bool is_same_process = old_rfh->GetProcess()->GetDeprecatedID() == |
| new_rfh->GetProcess()->GetDeprecatedID(); |
| |
| // Navigation.IsSameBrowsingInstance |
| if (is_main_frame) { |
| bool is_same_browsing_instance = |
| old_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| new_rfh->GetSiteInstance()); |
| |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsSameBrowsingInstance", |
| is_same_browsing_instance); |
| } |
| |
| // Navigation.IsSameSiteInstance |
| { |
| bool is_same_site_instance = |
| old_rfh->GetSiteInstance() == new_rfh->GetSiteInstance(); |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsSameSiteInstance", |
| is_same_site_instance); |
| if (is_main_frame) { |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsSameSiteInstance.MainFrame", |
| is_same_site_instance); |
| } else { |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsSameSiteInstance.Subframe", |
| is_same_site_instance); |
| } |
| } |
| |
| // Navigation.IsLockedProcess |
| { |
| ProcessLock process_lock = new_rfh->GetProcess()->GetProcessLock(); |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsLockedProcess", |
| process_lock.is_locked_to_site()); |
| if (common_params.url.SchemeIsHTTPOrHTTPS()) { |
| UMA_HISTOGRAM_BOOLEAN("Navigation.IsLockedProcess.HTTPOrHTTPS", |
| process_lock.is_locked_to_site()); |
| } |
| } |
| |
| // Navigation.RequiresDedicatedProcess |
| { |
| UMA_HISTOGRAM_BOOLEAN( |
| "Navigation.RequiresDedicatedProcess", |
| new_rfh->GetSiteInstance()->RequiresDedicatedProcess()); |
| if (common_params.url.SchemeIsHTTPOrHTTPS()) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "Navigation.RequiresDedicatedProcess.HTTPOrHTTPS", |
| new_rfh->GetSiteInstance()->RequiresDedicatedProcess()); |
| } |
| } |
| |
| // TimeToReadyToCommit2 |
| { |
| constexpr std::optional<base::Process::Priority> kPriority = std::nullopt; |
| base::TimeDelta delta = |
| ready_to_commit_time - common_params.navigation_start; |
| ui::PageTransition transition = |
| ui::PageTransitionFromInt(common_params.transition); |
| |
| LOG_NAVIGATION_TIMING_HISTOGRAM("TimeToReadyToCommit2", transition, |
| kPriority, delta); |
| if (is_main_frame) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("TimeToReadyToCommit2.MainFrame", |
| transition, kPriority, delta); |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("TimeToReadyToCommit2.Subframe", |
| transition, kPriority, delta); |
| } |
| if (is_same_process) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("TimeToReadyToCommit2.SameProcess", |
| transition, kPriority, delta); |
| } else { |
| LOG_NAVIGATION_TIMING_HISTOGRAM("TimeToReadyToCommit2.CrossProcess", |
| transition, kPriority, delta); |
| } |
| if (did_receive_early_hints_before_cross_origin_redirect) { |
| LOG_NAVIGATION_TIMING_HISTOGRAM( |
| "TimeToReadyToCommit2.CrossOriginRedirectAfterEarlyHints", transition, |
| kPriority, delta); |
| } |
| } |
| |
| // Navigation.OriginAgentCluster |
| { |
| UMA_HISTOGRAM_ENUMERATION("Navigation.OriginAgentCluster.Result", |
| origin_agent_cluster_end_result); |
| } |
| |
| // Guest (<webview> tag) metrics. |
| { |
| base::UmaHistogramBoolean("Navigation.IsGuest", |
| new_rfh->GetSiteInstance()->IsGuest()); |
| if (new_rfh->GetSiteInstance()->IsGuest()) { |
| base::UmaHistogramBoolean("Navigation.Guest.IsHTTPOrHTTPS", |
| common_params.url.SchemeIsHTTPOrHTTPS()); |
| base::UmaHistogramBoolean("Navigation.Guest.IsMainFrame", is_main_frame); |
| } |
| } |
| } |
| |
| // Convert the navigation type to the appropriate cross-document one. |
| // |
| // This is currently used when: |
| // 1) Restarting a same-document navigation as cross-document. |
| // 2) Failing a navigation and committing an error page. |
| blink::mojom::NavigationType ConvertToCrossDocumentType( |
| blink::mojom::NavigationType type) { |
| switch (type) { |
| case blink::mojom::NavigationType::SAME_DOCUMENT: |
| return blink::mojom::NavigationType::DIFFERENT_DOCUMENT; |
| case blink::mojom::NavigationType::HISTORY_SAME_DOCUMENT: |
| return blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT; |
| case blink::mojom::NavigationType::RELOAD: |
| case blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE: |
| case blink::mojom::NavigationType::RESTORE: |
| case blink::mojom::NavigationType::RESTORE_WITH_POST: |
| case blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT: |
| case blink::mojom::NavigationType::DIFFERENT_DOCUMENT: |
| return type; |
| } |
| } |
| |
| base::debug::CrashKeyString* GetNavigationRequestUrlCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "navigation_request_url", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetNavigationRequestInitiatorCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "navigation_request_initiator", base::debug::CrashKeySize::Size64); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetNavigationRequestIsSameDocumentCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "navigation_request_is_same_document", base::debug::CrashKeySize::Size64); |
| return crash_key; |
| } |
| |
| // Start a new nested async event with the given name. |
| void EnterChildTraceEvent(const char* name, NavigationRequest* request) { |
| // Passing nullptr as the event name will match the end event with the last |
| // unmatched begin event. |
| TRACE_EVENT_NESTABLE_ASYNC_END0("navigation", nullptr, |
| request->GetNavigationId()); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("navigation", name, |
| request->GetNavigationId()); |
| } |
| |
| // Start a new nested async event with the given name and args. |
| template <typename ArgType> |
| void EnterChildTraceEvent(const char* name, |
| NavigationRequest* request, |
| const char* arg_name, |
| ArgType arg_value) { |
| // Passing nullptr as the event name will match the end event with the last |
| // unmatched begin event. |
| TRACE_EVENT_NESTABLE_ASYNC_END0("navigation", nullptr, |
| request->GetNavigationId()); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1( |
| "navigation", name, request->GetNavigationId(), arg_name, arg_value); |
| } |
| |
| network::mojom::RequestDestination GetDestinationFromFrameTreeNode( |
| FrameTreeNode* frame_tree_node) { |
| if (frame_tree_node->IsInFencedFrameTree()) |
| return network::mojom::RequestDestination::kFencedframe; |
| |
| if (frame_tree_node->IsMainFrame()) { |
| return network::mojom::RequestDestination::kDocument; |
| } |
| |
| switch (frame_tree_node->frame_owner_element_type()) { |
| case blink::FrameOwnerElementType::kObject: |
| return network::mojom::RequestDestination::kObject; |
| case blink::FrameOwnerElementType::kEmbed: |
| return network::mojom::RequestDestination::kEmbed; |
| case blink::FrameOwnerElementType::kIframe: |
| return network::mojom::RequestDestination::kIframe; |
| case blink::FrameOwnerElementType::kFrame: |
| return network::mojom::RequestDestination::kFrame; |
| // Main frames are handled above. |
| case blink::FrameOwnerElementType::kNone: |
| NOTREACHED(); |
| case blink::FrameOwnerElementType::kFencedframe: |
| // Fenced frames are handled above. |
| NOTREACHED(); |
| } |
| } |
| |
| // Returns true if the parent's COEP policy `parent_coep` should block a child |
| // embedded in an <iframe> loaded with `child_coep` policy. The |
| // `is_credentialless` parameter reflects whether the child will be loaded as a |
| // credentialless document. |
| bool CoepBlockIframe(network::mojom::CrossOriginEmbedderPolicyValue parent_coep, |
| network::mojom::CrossOriginEmbedderPolicyValue child_coep, |
| bool is_credentialless) { |
| return !is_credentialless && |
| (network::CompatibleWithCrossOriginIsolated(parent_coep) && |
| !network::CompatibleWithCrossOriginIsolated(child_coep)); |
| } |
| |
| // Computes the history offset of the new document compared to the current one. |
| int EstimateHistoryOffset(NavigationController& controller, |
| bool should_replace_current_entry) { |
| if (should_replace_current_entry) |
| return 0; |
| |
| int current_index = controller.GetLastCommittedEntryIndex(); |
| int pending_index = controller.GetPendingEntryIndex(); |
| |
| // +1 for non history navigation. |
| if (current_index == -1 || pending_index == -1) |
| return 1; |
| |
| return pending_index - current_index; |
| } |
| |
| bool IsDocumentToCommitAnonymous(FrameTreeNode* frame, |
| bool is_synchronous_about_blank_navigation) { |
| // FencedFrame do not propagate the credentialless bit deeper. |
| // In particular, it means their future response will have to adhere to COEP. |
| if (frame->IsFencedFrameRoot()) |
| return false; |
| |
| RenderFrameHostImpl* current_document = frame->current_frame_host(); |
| RenderFrameHostImpl* parent_document = frame->parent(); |
| |
| // The synchronous about:blank navigation preserves the state of the initial |
| // empty document. |
| // TODO(https://github.com/whatwg/html/issues/6863): Remove the synchronous |
| // about:blank navigation. |
| if (is_synchronous_about_blank_navigation) |
| return current_document->IsCredentialless(); |
| |
| // The document to commit will be credentialless if either the iframe element |
| // has the 'credentialless' attribute set or the parent document is |
| // credentialless. |
| bool parent_is_credentialless = |
| parent_document && parent_document->IsCredentialless(); |
| return parent_is_credentialless || frame->Credentialless(); |
| } |
| |
| // Returns the "loading" URL in the renderer. This tries to replicate |
| // RenderFrameImpl::GetLoadingUrl(). This might return a different URL from |
| // what we get when calling GetLastCommittedURL() on `rfh`, in case the |
| // document had changed its URL through document.open() before, or |
| // when calling last_document_url_in_renderer(), in case of error pages and |
| // loadDataWithBaseURL documents. |
| // This function should only be used to preserve calculations that were |
| // previously done in the renderer but got moved to the browser (e.g. URL |
| // comparisons to determine if a navigation should do a replacement or not). |
| const GURL& GetLastLoadingURLInRendererForNavigationReplacement( |
| RenderFrameHostImpl* rfh) { |
| // Handle some special cases: |
| // - The "loading URL" for an error page commit is the URL that it failed to |
| // load. This will be retained as long as the document stays the same. |
| // - For loadDataWithBaseURL() navigations the "loading URL" will be the |
| // last committed URL (the data: URL). This will also be retained as long as |
| // the document stays the same. |
| if (rfh->IsErrorDocument() || |
| rfh->was_loaded_from_load_data_with_base_url()) { |
| return rfh->GetLastCommittedURL(); |
| } |
| |
| // Otherwise, return the last document URL. |
| return rfh->last_document_url_in_renderer(); |
| } |
| |
| bool IsOptedInFencedFrame(const net::HttpResponseHeaders& http_headers) { |
| network::mojom::SupportsLoadingModePtr result = |
| network::ParseSupportsLoadingMode(http_headers); |
| return !result.is_null() && |
| base::Contains(result->supported_modes, |
| network::mojom::LoadingMode::kFencedFrame); |
| } |
| |
| // If there are any "Origin-Trial" headers on the |response|, persist those |
| // that correspond to persistent origin trials, provided the tokens are valid. |
| void PersistOriginTrialsFromHeaders( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const network::mojom::URLResponseHead* response, |
| BrowserContext* browser_context, |
| ukm::SourceId source_id) { |
| // It is not possible to serialize opaque origins, so we cannot save any |
| // information for them. |
| if (origin.opaque()) |
| return; |
| |
| // Skip About:blank, about:srcdoc and a few other URLs, because they can't |
| // have any Origin-Trial header. |
| if (!response || !response->headers) |
| return; |
| |
| OriginTrialsControllerDelegate* origin_trials_delegate = |
| browser_context->GetOriginTrialsControllerDelegate(); |
| if (!origin_trials_delegate) |
| return; |
| |
| std::vector<std::string> tokens = |
| GetOriginTrialHeaderValues(response->headers.get()); |
| origin_trials_delegate->PersistTrialsFromTokens( |
| origin, partition_origin, tokens, base::Time::Now(), source_id); |
| } |
| |
| struct TopicsHeaderValueResult { |
| bool topics_eligible = false; |
| std::optional<std::string> header_value; |
| }; |
| |
| // Returns the topics header for a navigation request. Returns std::nullopt if |
| // the request isn't eligible for topics. This should align with the handling in |
| // `GetTopicsHeaderValueForSubresourceRequest()`. |
| TopicsHeaderValueResult GetTopicsHeaderValueForNavigationRequest( |
| FrameTreeNode* frame_tree_node, |
| const GURL& url) { |
| // Skip if the <iframe> does not have the "browsingtopics" opt-in attribute. |
| if (!frame_tree_node->browsing_topics()) { |
| return TopicsHeaderValueResult{}; |
| } |
| |
| RenderFrameHostImpl* rfh = frame_tree_node->current_frame_host(); |
| |
| // Skip top frame navigation. |
| // TODO(crbug.com/40260337): This should be checked at the mojom boundary of |
| // RenderFrameHostImpl::DidChangeIframeAttributes, and should be a DCHECK |
| // here. |
| if (rfh->is_main_frame()) { |
| return {}; |
| } |
| |
| // Skip fenced frames. |
| if (rfh->IsNestedWithinFencedFrame()) { |
| return {}; |
| } |
| |
| // Skip inactive pages (e.g. prerendered pages). |
| if (!rfh->GetPage().IsPrimary()) { |
| return {}; |
| } |
| |
| url::Origin origin = url::Origin::Create(url); |
| if (origin.opaque()) { |
| return {}; |
| } |
| |
| if (!network::IsOriginPotentiallyTrustworthy(origin)) { |
| return {}; |
| } |
| |
| const network::PermissionsPolicy* parent_policy = |
| rfh->GetParent()->GetPermissionsPolicy(); |
| |
| DCHECK(parent_policy); |
| |
| if (!parent_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature::kBrowsingTopics, origin) || |
| !parent_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature:: |
| kBrowsingTopicsBackwardCompatible, |
| origin)) { |
| return {}; |
| } |
| |
| std::vector<blink::mojom::EpochTopicPtr> topics; |
| bool topics_eligible = GetContentClient()->browser()->HandleTopicsWebApi( |
| origin, rfh->GetMainFrame(), |
| browsing_topics::ApiCallerSource::kIframeAttribute, |
| /*get_topics=*/true, |
| /*observe=*/false, topics); |
| |
| int num_versions_in_epochs = |
| topics_eligible |
| ? GetContentClient()->browser()->NumVersionsInTopicsEpochs( |
| rfh->GetMainFrame()) |
| : 0; |
| |
| return { |
| .topics_eligible = topics_eligible, |
| .header_value = DeriveTopicsHeaderValue(topics, num_versions_in_epochs)}; |
| } |
| |
| ukm::SourceId GetPageUkmSourceId(FrameTreeNode* frame_tree_node) { |
| CHECK(frame_tree_node); |
| RenderFrameHost* render_frame_host = frame_tree_node->current_frame_host(); |
| CHECK(render_frame_host); |
| // Our data collection policy disallows collecting UKMs while prerendering. |
| // So, return kInvalidSourceId when the page is in the prerendering state. |
| // See //content/browser/preloading/prerender/README.md and ask the team to |
| // explore options to record data for prerendering pages. |
| if (render_frame_host->IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)) { |
| return ukm::kInvalidSourceId; |
| } |
| return render_frame_host->GetPageUkmSourceId(); |
| } |
| |
| bool IsMhtmlMimeType(const std::string& mime_type) { |
| return mime_type == "multipart/related" || mime_type == "message/rfc822"; |
| } |
| |
| network::mojom::WebSandboxFlags GetSandboxFlagsInitiator( |
| const std::optional<blink::LocalFrameToken>& frame_token, |
| int initiator_process_id, |
| StoragePartitionImpl* storage_partition) { |
| if (!frame_token) { |
| return network::mojom::WebSandboxFlags::kNone; |
| } |
| |
| // Even if the navigation was initiated from an unload handler and the |
| // RenderFrameHost is gone, its associated PolicyContainerHost should be |
| // available by design. |
| // |
| // Note: See https://crbug.com/1473165. The "design" is currently not 100% |
| // achieved. The PolicyContainer might be missing when the navigation is |
| // initiated from RenderViewContextMenu::ExecuteCommand(...). |
| const PolicyContainerHost* policy_container_host = |
| RenderFrameHostImpl::GetPolicyContainerHost( |
| base::OptionalToPtr(frame_token), initiator_process_id, |
| storage_partition); |
| if (!policy_container_host) { |
| return network::mojom::WebSandboxFlags::kNone; |
| } |
| |
| return policy_container_host->policies().sandbox_flags; |
| } |
| |
| bool IsSharedStorageWritableEligibleForNavigationRequest( |
| FrameTreeNode* frame_tree_node, |
| const GURL& url) { |
| // False if the <iframe> does not have the "sharedstoragewritable" opt-in |
| // attribute. |
| if (!frame_tree_node->shared_storage_writable_opted_in()) { |
| return false; |
| } |
| |
| // Only child frames should have the `sharedstoragewritable` attribute set to |
| // true. |
| CHECK(!frame_tree_node->IsMainFrame()); |
| |
| // Apart from fenced frames' frame trees, skip non-primary pages (e.g. |
| // prerendered pages). |
| if (frame_tree_node->fenced_frame_status() != |
| RenderFrameHostImpl::FencedFrameStatus:: |
| kIframeNestedWithinFencedFrame && |
| (!frame_tree_node->frame_tree().is_primary() || |
| !frame_tree_node->frame_tree().root()->IsOutermostMainFrame())) { |
| return false; |
| } |
| |
| url::Origin origin = url::Origin::Create(url); |
| if (origin.opaque()) { |
| return false; |
| } |
| |
| if (!network::IsOriginPotentiallyTrustworthy(origin)) { |
| return false; |
| } |
| |
| CHECK(frame_tree_node->parent()); |
| const network::PermissionsPolicy* parent_policy = |
| frame_tree_node->parent()->GetPermissionsPolicy(); |
| |
| DCHECK(parent_policy); |
| return parent_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature::kSharedStorage, origin); |
| } |
| |
| std::optional<base::SafeRef<RenderFrameHostImpl>> |
| GetRenderFrameHostForBackForwardCacheRestore(FrameTreeNode* frame_tree_node, |
| NavigationEntryImpl* entry) { |
| if (!entry) { |
| return std::nullopt; |
| } |
| |
| auto restored_entry = frame_tree_node->navigator() |
| .controller() |
| .GetBackForwardCache() |
| .GetOrEvictEntry(entry->GetUniqueID()); |
| if (!restored_entry.has_value()) { |
| // If there is no active BFCache entry, we can't use the RFH from the |
| // BFCache entry for the history navigation. |
| return std::nullopt; |
| } |
| |
| RenderFrameHostImpl* restored_rfh = |
| restored_entry.value()->render_frame_host(); |
| |
| // If there is an ongoing BFCache NavigationRequest with the same entry, that |
| // NavigationRequest will be cancelled, and trigger an eviction from the |
| // NavigationRequest destructor (see comment there for details). So, we can't |
| // restore the to-be-evicted entry anymore. |
| NavigationRequest* previous_navigation_request = |
| frame_tree_node->navigation_request(); |
| if (previous_navigation_request && |
| previous_navigation_request->IsServedFromBackForwardCache() && |
| previous_navigation_request |
| ->GetRenderFrameHostRestoredFromBackForwardCache() == |
| restored_rfh) { |
| // Since the BFCache entry won't be restored, we evict it here with |
| // `kNavigationCancelledWhileRestoring` so that the NavigationRequest |
| // won't end up with not restored with no reason (or `Unknown` will be |
| // added instead). |
| // TODO(crbug.com/40283427): Only evict BFCache if the |
| // `BackForwardCacheCommitDeferringCondition`, which unfreezes the |
| // page and disables the eviction on the renderer side, is completed. |
| restored_rfh->EvictFromBackForwardCacheWithReason( |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kNavigationCancelledWhileRestoring); |
| return std::nullopt; |
| } |
| |
| if (!frame_tree_node->IsMainFrame()) { |
| // We have a matching BFCache entry for a subframe navigation. This |
| // shouldn't happen as we should've triggered deletion of BFCache |
| // entries that have the same BrowsingInstance as the current document. |
| // See https://crbug.com/1250111. |
| CaptureTraceForNavigationDebugScenario( |
| DebugScenario::kDebugBackForwardCacheEntryExistsOnSubframeHistoryNav); |
| return std::nullopt; |
| } |
| |
| return restored_rfh->GetSafeRef(); |
| } |
| |
| void MaybePrewarmHttpDiskCache( |
| BrowserContext& browser_context, |
| const GURL& url, |
| const std::optional<url::Origin>& initiator_origin) { |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kHttpDiskCachePrewarming) || |
| !blink::features::kHttpDiskCachePrewarmingTriggerOnNavigation.Get()) { |
| return; |
| } |
| |
| if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| |
| GetContentClient()->browser()->MaybePrewarmHttpDiskCache( |
| browser_context, initiator_origin, url); |
| } |
| |
| // Returns true in cases where an attempted download will end up replacing the |
| // current document anyway, due to showing an error page. |
| bool IsFailedDownload(bool is_download, |
| const net::HttpResponseHeaders* headers) { |
| return is_download && headers && |
| !network::IsSuccessfulStatus(headers->response_code()); |
| } |
| |
| // Returns if the given `rfh` should be evicted from BackForwardCache due to |
| // ongoing navigation. |
| bool MaybeEvictFromBackForwardCacheBySubframeNavigation( |
| RenderFrameHostImpl* rfh) { |
| if (rfh->GetParentOrOuterDocument() && |
| rfh->GetLifecycleState() == |
| RenderFrameHost::LifecycleState::kInBackForwardCache) { |
| // Normally, ongoing subframe navigations will be deferred by |
| // `BackForwardCacheSubframeNavigationThrottle` before they reach this |
| // point, if the page the subframe is on gets BFCached. |
| // |
| // However, it's possible for subframe navigations to end up here while its |
| // page is BFCached, if at the time the navigation went through |
| // `BackForwardCacheSubframeNavigationThrottle::WillStartRequest()` or |
| // BackForwardCacheSubframeNavigationThrottle::WillCommitWithoutUrlLoader(), |
| // the page is not BFCached yet, but then the page gets BFCached in between |
| // that time and when this function is called. |
| // |
| // Outside of tests, this should not be possible, as |
| // `BackForwardCacheSubframeNavigationThrottle` are the last throttles to be |
| // registered/run. However, in tests, the last throttles to run are |
| // test-only throttles, which can introduce an asynchronous step, making it |
| // possible for the page to enter BFCache during that time. In that case, we |
| // shouldn't continue processing the navigation in the subframe and need to |
| // evict the page from BFCache. |
| rfh->EvictFromBackForwardCacheWithReason( |
| BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating); |
| |
| // DO NOT ADD CODE after this. The previous call has destroyed the |
| // NavigationRequest. |
| return true; |
| } |
| return false; |
| } |
| |
| net::StorageAccessApiStatus ShouldLoadWithStorageAccess( |
| const blink::mojom::BeginNavigationParams& begin_params, |
| const blink::mojom::CommonNavigationParams& common_params, |
| const RenderFrameHostImpl* previous_document_rfh, |
| bool did_encounter_cross_origin_redirect, |
| const GURL response_url, |
| const network::mojom::URLResponseHead* response) { |
| // Experimental: Storage Access API Headers |
| // (https://github.com/cfredric/storage-access-headers) |
| // |
| // A server can opt-in to provide storage access to a document by setting the |
| // `Activate-Storage-Access: load` header, provided that the user has already |
| // granted the relevant `storage-access` permission. |
| // |
| // Note: As of today, `about:blank`, `about:srcdoc`, and MHTML-iframe do not |
| // have a response. |
| if (response && response->load_with_storage_access) { |
| // TODO(https://crbug.com/344608182): this ought to use a dedicated status, |
| // since the JS API was *not* used to get storage access here. |
| return net::StorageAccessApiStatus::kAccessViaAPI; |
| } |
| |
| // Storage Access API: https://privacycg.github.io/storage-access/#navigation |
| // |
| // If a document has storage access, and initiates a navigation in the same |
| // frame toward a document from the same origin, the `has storage access` bit |
| // is inherited. |
| // |
| // This doesn't hold if there is a cross-origin redirect in between. |
| // |
| // Note: `begin_params` and `common_params` are not trusted, so we have to |
| // check the frame token. |
| switch (begin_params.storage_access_api_status) { |
| case net::StorageAccessApiStatus::kNone: |
| return net::StorageAccessApiStatus::kNone; |
| case net::StorageAccessApiStatus::kAccessViaAPI: |
| return common_params.initiator_origin && |
| common_params.initiator_origin->IsSameOriginWith( |
| response_url) && |
| begin_params.initiator_frame_token && |
| begin_params.initiator_frame_token == |
| previous_document_rfh->GetFrameToken() && |
| !did_encounter_cross_origin_redirect |
| ? begin_params.storage_access_api_status |
| : net::StorageAccessApiStatus::kNone; |
| } |
| } |
| |
| // The sampling rate for UKM. |
| constexpr double kUkmSamplingRate = 0.001; |
| |
| } // namespace |
| |
| NavigationRequest::PrerenderActivationNavigationState:: |
| PrerenderActivationNavigationState() = default; |
| NavigationRequest::PrerenderActivationNavigationState:: |
| ~PrerenderActivationNavigationState() = default; |
| |
| // static |
| std::unique_ptr<NavigationRequest> NavigationRequest::CreateBrowserInitiated( |
| FrameTreeNode* frame_tree_node, |
| blink::mojom::CommonNavigationParamsPtr common_params, |
| blink::mojom::CommitNavigationParamsPtr commit_params, |
| bool was_opener_suppressed, |
| const std::string& extra_headers, |
| FrameNavigationEntry* frame_entry, |
| NavigationEntryImpl* entry, |
| bool is_form_submission, |
| std::unique_ptr<NavigationUIData> navigation_ui_data, |
| const std::optional<blink::Impression>& impression, |
| bool is_pdf, |
| bool is_embedder_initiated_fenced_frame_navigation, |
| std::optional<std::u16string> embedder_shared_storage_context) { |
| auto request = Create( |
| frame_tree_node, std::move(common_params), std::move(commit_params), |
| /*browser_initiated=*/true, was_opener_suppressed, |
| std::nullopt /* initiator_frame_token */, |
| ChildProcessHost::kInvalidUniqueID /* initiator_process_id */, |
| extra_headers, frame_entry, entry, is_form_submission, |
| std::move(navigation_ui_data), impression, |
| blink::mojom::NavigationInitiatorActivationAndAdStatus:: |
| kDidNotStartWithTransientActivation, |
| is_pdf, is_embedder_initiated_fenced_frame_navigation, |
| /*is_container_initiated=*/false, /*has_rel_opener=*/false, |
| net::StorageAccessApiStatus::kNone, embedder_shared_storage_context); |
| // It is only possible for a null NavigationRequest to be returned if an |
| // initiator_frame_token is provided. |
| CHECK(request); |
| return request; |
| } |
| |
| // static |
| std::unique_ptr<NavigationRequest> NavigationRequest::Create( |
| FrameTreeNode* frame_tree_node, |
| blink::mojom::CommonNavigationParamsPtr common_params, |
| blink::mojom::CommitNavigationParamsPtr commit_params, |
| bool browser_initiated, |
| bool was_opener_suppressed, |
| const std::optional<blink::LocalFrameToken>& initiator_frame_token, |
| int initiator_process_id, |
| const std::string& extra_headers, |
| FrameNavigationEntry* frame_entry, |
| NavigationEntryImpl* entry, |
| bool is_form_submission, |
| std::unique_ptr<NavigationUIData> navigation_ui_data, |
| const std::optional<blink::Impression>& impression, |
| blink::mojom::NavigationInitiatorActivationAndAdStatus |
| initiator_activation_and_ad_status, |
| bool is_pdf, |
| bool is_embedder_initiated_fenced_frame_navigation, |
| bool is_container_initiated, |
| bool has_rel_opener, |
| net::StorageAccessApiStatus storage_access_api_status, |
| std::optional<std::u16string> embedder_shared_storage_context) { |
| TRACE_EVENT1("navigation", "NavigationRequest::Create", "browser_initiated", |
| browser_initiated); |
| |
| common_params->request_destination = |
| GetDestinationFromFrameTreeNode(frame_tree_node); |
| |
| auto navigation_params = blink::mojom::BeginNavigationParams::New( |
| initiator_frame_token, extra_headers, net::LOAD_NORMAL, |
| false /* skip_service_worker */, |
| blink::mojom::RequestContextType::LOCATION, |
| blink::mojom::MixedContentContextType::kBlockable, is_form_submission, |
| false /* was_initiated_by_link_click */, |
| blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */, |
| std::string() /* searchable_form_encoding */, |
| GURL() /* client_side_redirect_url */, |
| std::nullopt /* devtools_initiator_info */, |
| nullptr /* trust_token_params */, impression, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| initiator_activation_and_ad_status, is_container_initiated, |
| storage_access_api_status, has_rel_opener); |
| |
| // Shift-Reload forces bypassing caches and service workers. |
| if (common_params->navigation_type == |
| blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE) { |
| navigation_params->load_flags |= net::LOAD_BYPASS_CACHE; |
| navigation_params->skip_service_worker = true; |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory; |
| if (frame_entry) { |
| blob_url_loader_factory = frame_entry->blob_url_loader_factory(); |
| |
| if (common_params->url.SchemeIsBlob() && !blob_url_loader_factory) { |
| // If this navigation entry came from session history then the blob |
| // factory would have been cleared in |
| // NavigationEntryImpl::ResetForCommit(). This is avoid keeping large |
| // blobs alive unnecessarily and the spec is unclear. So create a new blob |
| // factory which will work if the blob happens to still be alive, |
| // resolving the blob URL in the site instance it was loaded in. |
| blob_url_loader_factory = |
| ChromeBlobStorageContext::URLLoaderFactoryForUrl( |
| frame_tree_node->navigator() |
| .controller() |
| .GetBrowserContext() |
| ->GetStoragePartition(frame_entry->site_instance()), |
| common_params->url); |
| } |
| } |
| |
| // Ensure that top-level navigations initiated from fenced frames (such as |
| // _unfencedTop, pop-ups, and "Open Link in...") fail if the fenced frame's |
| // network access is revoked. Some of these paths have additional checks for |
| // UX reasons (see: RenderFrameHostImpl::CreateNewWindow and |
| // RenderViewContextMenu::IsCommandIdEnabled). This check is only needed for |
| // navigations that escape the fenced frame boundary, as the target will have |
| // no knowledge of the fenced frame's network revocation nonce. This check |
| // does not exist in CreateRendererInitiated() because that path is not hit |
| // for navigations that cross fenced frame boundaries. |
| if (initiator_frame_token) { |
| // It is okay to use the current frame host's storage partition because |
| // the storage partition does not change over the lifetime of the fenced |
| // frame. |
| std::optional<bool> is_untrusted_network_disabled = |
| RenderFrameHostImpl::GetIsUntrustedNetworkDisabled( |
| base::OptionalToPtr(initiator_frame_token), initiator_process_id, |
| static_cast<StoragePartitionImpl*>( |
| frame_tree_node->current_frame_host()->GetStoragePartition())); |
| if (is_untrusted_network_disabled == true) { |
| frame_tree_node->current_frame_host()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Navigations cannot be initiated from a fenced frame after its " |
| "network has been disabled."); |
| return nullptr; |
| } |
| } |
| |
| std::unique_ptr<NavigationRequest> navigation_request(new NavigationRequest( |
| frame_tree_node, std::move(common_params), std::move(navigation_params), |
| std::move(commit_params), browser_initiated, |
| false /* from_begin_navigation */, |
| false /* is_synchronous_renderer_commit */, frame_entry, entry, |
| std::move(navigation_ui_data), std::move(blob_url_loader_factory), |
| mojo::NullAssociatedRemote(), |
| nullptr /* prefetched_signed_exchange_cache */, |
| GetRenderFrameHostForBackForwardCacheRestore(frame_tree_node, entry), |
| initiator_process_id, was_opener_suppressed, is_pdf, |
| is_embedder_initiated_fenced_frame_navigation, |
| mojo::NullReceiver() /* renderer_cancellation_listener */, |
| embedder_shared_storage_context)); |
| |
| return navigation_request; |
| } |
| |
| // static |
| std::unique_ptr<NavigationRequest> NavigationRequest::CreateRendererInitiated( |
| FrameTreeNode* frame_tree_node, |
| NavigationEntryImpl* entry, |
| blink::mojom::CommonNavigationParamsPtr common_params, |
| blink::mojom::BeginNavigationParamsPtr begin_params, |
| int current_history_list_index, |
| int current_history_list_length, |
| bool override_user_agent, |
| scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory, |
| mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client, |
| scoped_refptr<PrefetchedSignedExchangeCache> |
| prefetched_signed_exchange_cache, |
| mojo::PendingReceiver<mojom::NavigationRendererCancellationListener> |
| renderer_cancellation_listener) { |
| TRACE_EVENT0("navigation", "NavigationRequest::CreateRendererInitiated"); |
| // Only normal navigations to a different document or reloads are expected. |
| // - Renderer-initiated same document navigations never start in the browser. |
| // - Restore-navigations are always browser-initiated. |
| // - History-navigations use the browser-initiated path, even the ones that |
| // are initiated by a javascript script. |
| DCHECK(NavigationTypeUtils::IsReload(common_params->navigation_type) || |
| common_params->navigation_type == |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT); |
| |
| common_params->request_destination = |
| GetDestinationFromFrameTreeNode(frame_tree_node); |
| |
| // TODO(clamy): See if the navigation start time should be measured in the |
| // renderer and sent to the browser instead of being measured here. |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::mojom::CommitNavigationParams::New( |
| url::Origin(), |
| // The correct storage key will be computed before committing the |
| // navigation. |
| blink::StorageKey(), override_user_agent, |
| /*redirects=*/std::vector<GURL>(), |
| /*redirect_response=*/ |
| std::vector<network::mojom::URLResponseHeadPtr>(), |
| /*redirect_infos=*/std::vector<net::RedirectInfo>(), |
| /*post_content_type=*/std::string(), common_params->url, |
| common_params->method, |
| /*can_load_local_resources=*/false, |
| /*page_state=*/std::string(), |
| /*nav_entry_id=*/0, |
| /*subframe_unique_names=*/base::flat_map<std::string, bool>(), |
| /*intended_as_new_entry=*/false, |
| // Set to -1 because history-navigations do not use this path. See |
| // comments above. |
| /*pending_history_list_index=*/-1, current_history_list_index, |
| current_history_list_length, |
| /*was_discarded=*/false, |
| /*is_view_source=*/false, |
| /*should_clear_history_list=*/false, |
| /*navigation_timing=*/blink::mojom::NavigationTiming::New(), |
| blink::mojom::WasActivatedOption::kUnknown, |
| /*navigation_token=*/base::UnguessableToken::Create(), |
| /*prefetched_signed_exchanges=*/ |
| std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr>(), |
| #if BUILDFLAG(IS_ANDROID) |
| /*data_url_as_string=*/std::string(), |
| #endif |
| /*is_browser_initiated=*/false, |
| /*has_ua_visual_transition*/ false, |
| /*document_ukm_source_id=*/ukm::kInvalidSourceId, |
| frame_tree_node->pending_frame_policy(), |
| /*force_enabled_origin_trials=*/std::vector<std::string>(), |
| /*origin_agent_cluster=*/false, |
| /*origin_agent_cluster_left_as_default=*/true, |
| /*enabled_client_hints=*/ |
| std::vector<network::mojom::WebClientHintsType>(), |
| /*is_cross_site_cross_browsing_context_group=*/false, |
| /*should_have_sticky_user_activation=*/false, |
| /*old_page_info=*/nullptr, /*http_response_code=*/-1, |
| blink::mojom::NavigationApiHistoryEntryArrays::New(), |
| /*early_hints_preloaded_resources=*/std::vector<GURL>(), |
| // This timestamp will be populated when the commit IPC is sent. |
| /*commit_sent=*/base::TimeTicks(), /*srcdoc_value=*/std::string(), |
| /*should_load_data_url=*/false, |
| /*ancestor_or_self_has_cspee=*/ |
| frame_tree_node->AncestorOrSelfHasCSPEE(), |
| /*reduced_accept_language=*/std::string(), |
| /*navigation_delivery_type=*/ |
| network::mojom::NavigationDeliveryType::kDefault, |
| /*view_transition_state=*/std::nullopt, |
| /*soft_navigation_heuristics_task_id=*/std::nullopt, |
| /*modified_runtime_features=*/ |
| base::flat_map<::blink::mojom::RuntimeFeature, bool>(), |
| /*fenced_frame_properties=*/std::nullopt, |
| /*not_restored_reasons=*/nullptr, |
| /*load_with_storage_access=*/ |
| net::StorageAccessApiStatus::kNone, |
| /*browsing_context_group_info=*/std::nullopt, |
| /*lcpp_hint=*/nullptr, blink::CreateDefaultRendererContentSettings(), |
| /*cookie_deprecation_label=*/std::nullopt, |
| /*visited_link_salt=*/std::nullopt, |
| /*local_surface_id=*/std::nullopt, |
| frame_tree_node->current_frame_host()->GetCachedPermissionStatuses(), |
| /*should_skip_screenshot=*/false, |
| /*force_new_document_sequence_number=*/false, |
| /*navigation_metrics_token=*/base::UnguessableToken::Create()); |
| |
| commit_params->navigation_timing->system_entropy_at_navigation_start = |
| SystemEntropyUtils::ComputeSystemEntropyForFrameTreeNode( |
| frame_tree_node, blink::mojom::SystemEntropy::kNormal); |
| |
| // CreateRendererInitiated() should only be triggered when the navigation is |
| // initiated by a frame in the same process. |
| // TODO(crbug.com/40686861): Find a way to DCHECK that the routing ID |
| // is from the current RFH. |
| int initiator_process_id = |
| frame_tree_node->current_frame_host()->GetProcess()->GetDeprecatedID(); |
| |
| // `was_opener_suppressed` can be true for renderer initiated navigations, but |
| // only in cases which get routed through `CreateBrowserInitiated()` instead. |
| std::unique_ptr<NavigationRequest> navigation_request(new NavigationRequest( |
| frame_tree_node, std::move(common_params), std::move(begin_params), |
| std::move(commit_params), |
| false, // browser_initiated |
| true, // from_begin_navigation |
| false, // is_synchronous_renderer_commit |
| nullptr, // frame_entry |
| entry, |
| nullptr, // navigation_ui_data |
| std::move(blob_url_loader_factory), std::move(navigation_client), |
| std::move(prefetched_signed_exchange_cache), |
| std::nullopt, // rfh_restored_from_back_forward_cache |
| initiator_process_id, |
| /*was_opener_suppressed=*/false, /*is_pdf=*/false, |
| /*is_embedder_initiated_fenced_frame_navigation=*/false, |
| std::move(renderer_cancellation_listener))); |
| |
| return navigation_request; |
| } |
| |
| // static |
| std::unique_ptr<NavigationRequest> |
| NavigationRequest::CreateForSynchronousRendererCommit( |
| FrameTreeNode* frame_tree_node, |
| RenderFrameHostImpl* render_frame_host, |
| bool is_same_document, |
| const GURL& url, |
| const url::Origin& origin, |
| const std::optional<GURL>& initiator_base_url, |
| const net::IsolationInfo& isolation_info_for_subresources, |
| blink::mojom::ReferrerPtr referrer, |
| const ui::PageTransition& transition, |
| bool should_replace_current_entry, |
| const std::string& method, |
| bool has_transient_activation, |
| bool is_overriding_user_agent, |
| const std::vector<GURL>& redirects, |
| const GURL& original_url, |
| std::unique_ptr<CrossOriginEmbedderPolicyReporter> coep_reporter, |
| std::unique_ptr<DocumentIsolationPolicyReporter> dip_reporter, |
| int http_response_code, |
| base::TimeTicks actual_navigation_start) { |
| TRACE_EVENT0("navigation", "NavigationRequest::CreateForSynchronousRendererCommit"); |
| // TODO(clamy): Improve the *NavigationParams and *CommitParams to avoid |
| // copying so many parameters here. |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| url, |
| // TODO(nasko): Investigate better value to pass for |
| // |initiator_origin|. |
| origin, initiator_base_url, std::move(referrer), transition, |
| is_same_document ? blink::mojom::NavigationType::SAME_DOCUMENT |
| : blink::mojom::NavigationType::DIFFERENT_DOCUMENT, |
| blink::NavigationDownloadPolicy(), should_replace_current_entry, |
| GURL() /* base_url_for_data_url*/, actual_navigation_start, |
| base::TimeTicks::Now() /* navigation_start */, method /* method */, |
| nullptr /* post_data */, network::mojom::SourceLocation::New(), |
| false /* started_from_context_menu */, has_transient_activation, |
| false /* has_text_fragment_token */, |
| network::mojom::CSPDisposition::CHECK, |
| std::vector<int>() /* initiator_origin_trial_features */, |
| std::string() /* href_translate */, |
| false /* is_history_navigation_in_new_child_frame */, |
| base::TimeTicks::Now() /* input_start */, |
| network::mojom::RequestDestination::kEmpty); |
| // Note that some params are set to default values (e.g. page_state set to |
| // the default blink::PageState()) even if the DidCommit message that came |
| // from the renderer contained relevant info that can be used to fill the |
| // params, because setting those values don't match with the pattern used |
| // by navigations that went through the browser (e.g. page_state is only |
| // set in CommitNavigationParams of history navigations) or these values are |
| // not used by the browser after commit. |
| blink::mojom::CommitNavigationParamsPtr commit_params = |
| blink::mojom::CommitNavigationParams::New( |
| url::Origin(), |
| // The correct storage key is computed right after creating the |
| // NavigationRequest below. |
| blink::StorageKey(), is_overriding_user_agent, redirects, |
| /*redirect_response=*/ |
| std::vector<network::mojom::URLResponseHeadPtr>(), |
| /*redirect_infos=*/std::vector<net::RedirectInfo>(), |
| /*post_content_type=*/std::string(), original_url, |
| /*original_method=*/method, |
| /*can_load_local_resources=*/false, |
| /*page_state=*/std::string(), |
| /*nav_entry_id=*/0, |
| /*subframe_unique_names=*/base::flat_map<std::string, bool>(), |
| /*intended_as_new_entry=*/false, |
| /*pending_history_list_index=*/-1, |
| /*current_history_list_index=*/-1, |
| /*current_history_list_length=*/-1, |
| /*was_discarded=*/false, |
| /*is_view_source=*/false, |
| /*should_clear_history_list=*/false, |
| /*navigation_timing=*/blink::mojom::NavigationTiming::New(), |
| blink::mojom::WasActivatedOption::kUnknown, |
| /*navigation_token=*/base::UnguessableToken::Create(), |
| /*prefetched_signed_exchanges=*/ |
| std::vector<blink::mojom::PrefetchedSignedExchangeInfoPtr>(), |
| #if BUILDFLAG(IS_ANDROID) |
| /*data_url_as_string=*/std::string(), |
| #endif |
| /*is_browser_initiated=*/false, |
| /*has_ua_visual_transition*/ false, |
| /*document_ukm_source_id=*/ukm::kInvalidSourceId, |
| frame_tree_node->pending_frame_policy(), |
| /*force_enabled_origin_trials=*/std::vector<std::string>(), |
| /*origin_agent_cluster=*/false, |
| /*origin_agent_cluster_left_as_default=*/true, |
| /*enabled_client_hints=*/ |
| std::vector<network::mojom::WebClientHintsType>(), |
| /*is_cross_site_cross_browsing_context_group=*/false, |
| /*should_have_sticky_user_activation=*/false, |
| /*old_page_info=*/nullptr, http_response_code, |
| blink::mojom::NavigationApiHistoryEntryArrays::New(), |
| /*early_hints_preloaded_resources=*/std::vector<GURL>(), |
| // This timestamp will be populated when the commit IPC is sent. |
| /*commit_sent=*/base::TimeTicks(), /*srcdoc_value=*/std::string(), |
| /*should_load_data_url=*/false, |
| /*ancestor_or_self_has_cspee=*/ |
| frame_tree_node->AncestorOrSelfHasCSPEE(), |
| /*reduced_accept_language=*/std::string(), |
| /*navigation_delivery_type=*/ |
| network::mojom::NavigationDeliveryType::kDefault, |
| /*view_transition_state=*/std::nullopt, |
| /*soft_navigation_heuristics_task_id=*/std::nullopt, |
| /*modified_runtime_features=*/ |
| base::flat_map<::blink::mojom::RuntimeFeature, bool>(), |
| /*fenced_frame_properties=*/std::nullopt, |
| /*not_restored_reasons=*/nullptr, |
| /*load_with_storage_access=*/ |
| net::StorageAccessApiStatus::kNone, |
| /*browsing_context_group_info=*/std::nullopt, |
| /*lcpp_hint=*/nullptr, blink::CreateDefaultRendererContentSettings(), |
| /*cookie_deprecation_label=*/std::nullopt, |
| /*visited_link_salt=*/std::nullopt, |
| /*local_surface_id=*/std::nullopt, |
| render_frame_host->GetCachedPermissionStatuses(), |
| /*should_skip_screenshot=*/false, |
| /*force_new_document_sequence_number=*/false, |
| /*navigation_metrics_token=*/base::UnguessableToken::Create()); |
| blink::mojom::BeginNavigationParamsPtr begin_params = |
| blink::mojom::BeginNavigationParams::New(); |
| std::unique_ptr<NavigationRequest> navigation_request(new NavigationRequest( |
| frame_tree_node, std::move(common_params), std::move(begin_params), |
| std::move(commit_params), false /* browser_initiated */, |
| false /* from_begin_navigation */, |
| true /* is_synchronous_renderer_commit */, |
| nullptr /* frame_navigation_entry */, nullptr /* navigation_entry */, |
| nullptr /* navigation_ui_data */, nullptr /* blob_url_loader_factory */, |
| mojo::NullAssociatedRemote(), |
| nullptr /* prefetched_signed_exchange_cache */, |
| std::nullopt /* rfh_restored_from_back_forward_cache */, |
| ChildProcessHost::kInvalidUniqueID /* initiator_process_id */, |
| false /* was_opener_suppressed */, false /* is_pdf */)); |
| |
| std::optional<base::UnguessableToken> nonce = render_frame_host->ComputeNonce( |
| navigation_request->is_credentialless(), |
| navigation_request->ComputeFencedFrameNonce()); |
| url::Origin top_level_origin = |
| render_frame_host->ComputeTopFrameOrigin(origin); |
| if (nonce) { |
| // If the nonce isn't null, we can use the simpler form of the constructor. |
| navigation_request->commit_params_->storage_key = |
| blink::StorageKey::CreateWithNonce(origin, *nonce); |
| } else { |
| // Otherwise we need to derive the top_level_site and ancestor_chain_bit. |
| net::SchemefulSite top_level_site(top_level_origin); |
| |
| blink::mojom::AncestorChainBit ancestor_chain_bit = |
| blink::mojom::AncestorChainBit::kSameSite; |
| if (render_frame_host->ComputeSiteForCookies().IsNull() || |
| !top_level_site.IsSameSiteWith(origin) || !top_level_site.opaque() || |
| origin.opaque()) { |
| ancestor_chain_bit = blink::mojom::AncestorChainBit::kCrossSite; |
| } |
| |
| navigation_request->commit_params_->storage_key = |
| blink::StorageKey::Create(origin, top_level_site, ancestor_chain_bit); |
| } |
| navigation_request->commit_params_->navigation_timing |
| ->system_entropy_at_navigation_start = |
| SystemEntropyUtils::ComputeSystemEntropyForFrameTreeNode( |
| frame_tree_node, blink::mojom::SystemEntropy::kNormal); |
| navigation_request->render_frame_host_ = render_frame_host->GetSafeRef(); |
| navigation_request->coep_reporter_ = std::move(coep_reporter); |
| navigation_request->dip_reporter_ = std::move(dip_reporter); |
| navigation_request->isolation_info_for_subresources_ = |
| isolation_info_for_subresources; |
| navigation_request->associated_rfh_type_ = |
| AssociatedRenderFrameHostType::CURRENT; |
| navigation_request->StartNavigation(); |
| DCHECK(navigation_request->IsNavigationStarted()); |
| |
| return navigation_request; |
| } |
| |
| // static class variable used to generate unique navigation ids for |
| // NavigationRequest. |
| int64_t NavigationRequest::unique_id_counter_ = 0; |
| |
| NavigationRequest::NavigationRequest( |
| FrameTreeNode* frame_tree_node, |
| blink::mojom::CommonNavigationParamsPtr common_params, |
| blink::mojom::BeginNavigationParamsPtr begin_params, |
| blink::mojom::CommitNavigationParamsPtr commit_params, |
| bool browser_initiated, |
| bool from_begin_navigation, |
| bool is_synchronous_renderer_commit, |
| const FrameNavigationEntry* frame_entry, |
| NavigationEntryImpl* entry, |
| std::unique_ptr<NavigationUIData> navigation_ui_data, |
| scoped_refptr<network::SharedURLLoaderFactory> blob_url_loader_factory, |
| mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client, |
| scoped_refptr<PrefetchedSignedExchangeCache> |
| prefetched_signed_exchange_cache, |
| std::optional<base::SafeRef<RenderFrameHostImpl>> |
| rfh_restored_from_back_forward_cache, |
| int initiator_process_id, |
| bool was_opener_suppressed, |
| bool is_pdf, |
| bool is_embedder_initiated_fenced_frame_navigation, |
| mojo::PendingReceiver<mojom::NavigationRendererCancellationListener> |
| renderer_cancellation_listener, |
| std::optional<std::u16string> embedder_shared_storage_context) |
| : frame_tree_node_(frame_tree_node), |
| is_synchronous_renderer_commit_(is_synchronous_renderer_commit), |
| common_params_(std::move(common_params)), |
| begin_params_(std::move(begin_params)), |
| commit_params_(std::move(commit_params)), |
| navigation_ui_data_(std::move(navigation_ui_data)), |
| blob_url_loader_factory_(std::move(blob_url_loader_factory)), |
| restore_type_(entry ? entry->restore_type() : RestoreType::kNotRestored), |
| // Some navigations, such as renderer-initiated subframe navigations, |
| // won't have a NavigationEntryImpl. Set |reload_type_| if applicable |
| // for them. |
| reload_type_( |
| entry ? entry->reload_type() |
| : NavigationTypeToReloadType(common_params_->navigation_type)), |
| nav_entry_id_(entry ? entry->GetUniqueID() : 0), |
| from_begin_navigation_(from_begin_navigation), |
| site_info_( |
| frame_tree_node_->navigator().controller().GetBrowserContext()), |
| navigation_entry_offset_( |
| EstimateHistoryOffset(frame_tree_node_->navigator().controller(), |
| common_params_->should_replace_current_entry)), |
| prefetched_signed_exchange_cache_( |
| std::move(prefetched_signed_exchange_cache)), |
| rfh_restored_from_back_forward_cache_( |
| rfh_restored_from_back_forward_cache), |
| is_back_forward_cache_restore_( |
| rfh_restored_from_back_forward_cache.has_value()), |
| // Store the old RenderFrameHost id at request creation to be used later. |
| current_render_frame_host_id_at_construction_( |
| frame_tree_node->current_frame_host()->GetGlobalId()), |
| initiator_frame_token_(begin_params_->initiator_frame_token), |
| initiator_process_id_(initiator_process_id), |
| sandbox_flags_initiator_(GetSandboxFlagsInitiator( |
| initiator_frame_token_, |
| initiator_process_id, |
| static_cast<StoragePartitionImpl*>( |
| GetStoragePartitionWithCurrentSiteInfo()))), |
| was_opener_suppressed_(was_opener_suppressed), |
| is_credentialless_( |
| IsDocumentToCommitAnonymous(frame_tree_node, |
| is_synchronous_renderer_commit)), |
| previous_page_ukm_source_id_(GetPageUkmSourceId(frame_tree_node_)), |
| cookie_observers_(std::make_unique<CookieAccessObservers>( |
| base::BindRepeating(&NavigationRequest::NotifyCookiesAccessed, |
| base::Unretained(this)))), |
| is_pdf_(is_pdf), |
| is_embedder_initiated_fenced_frame_navigation_( |
| is_embedder_initiated_fenced_frame_navigation), |
| fenced_frame_properties_( |
| is_embedder_initiated_fenced_frame_navigation |
| ? std::make_optional(FencedFrameProperties(common_params_->url)) |
| : std::nullopt), |
| embedder_shared_storage_context_(embedder_shared_storage_context), |
| has_ad_auction_headers_attribute_(frame_tree_node->ad_auction_headers()), |
| request_method_(common_params_->method) { |
| TRACE_EVENT_WITH_FLOW1("navigation", "NavigationRequest::NavigationRequest", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_OUT, "navigation_request", this); |
| CHECK(!common_params_->initiator_base_url || |
| !common_params_->initiator_base_url->is_empty()); |
| DCHECK(!blink::IsRendererDebugURL(common_params_->url)); |
| DCHECK(common_params_->method == "POST" || !common_params_->post_data); |
| DCHECK_EQ(common_params_->url, commit_params_->original_url); |
| // Navigations can't be a replacement and a reload at the same time. |
| DCHECK(!common_params_->should_replace_current_entry || |
| !NavigationTypeUtils::IsReload(common_params_->navigation_type)); |
| DCHECK(IsInOutermostMainFrame() || |
| common_params_->base_url_for_data_url.is_empty()); |
| #if BUILDFLAG(IS_ANDROID) |
| DCHECK(IsInOutermostMainFrame() || |
| commit_params_->data_url_as_string.empty()); |
| #endif |
| CheckSoftNavigationHeuristicsInvariants(); |
| |
| ScopedCrashKeys crash_keys(*this); |
| |
| ComputeDownloadPolicy(); |
| |
| // Ensure the blink::RuntimeFeatureStateContext is initialized. |
| runtime_feature_state_context_ = blink::RuntimeFeatureStateContext(); |
| |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("navigation", "NavigationRequest", |
| navigation_id_, "navigation_request", this); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("navigation", "Initializing", |
| navigation_id_); |
| |
| if (GetInitiatorFrameToken().has_value()) { |
| RenderFrameHostImpl* initiator_rfh = RenderFrameHostImpl::FromFrameToken( |
| GetInitiatorProcessId(), GetInitiatorFrameToken().value()); |
| if (initiator_rfh) |
| initiator_document_token_ = initiator_rfh->GetDocumentToken(); |
| } |
| |
| // Spec: https://github.com/whatwg/html/issues/8846 |
| // We only allow the parent to access a subframe resource timing if the |
| // navigation is container-initiated, e.g. iframe changed src. |
| if (begin_params_->is_container_initiated) { |
| // Only same-origin navigations without cross-origin redirects can |
| // expose response details (status-code / mime-type). |
| // https://github.com/whatwg/fetch/issues/1602 |
| // Note that this condition checks this navigation is not cross origin. |
| // Cross-origin redirects are checked as part of OnRequestRedirected(). |
| commit_params_->navigation_timing->parent_resource_timing_access = |
| GetParentFrame()->GetLastCommittedOrigin().IsSameOriginWith(GetURL()) |
| ? blink::mojom::ParentResourceTimingAccess:: |
| kReportWithResponseDetails |
| : blink::mojom::ParentResourceTimingAccess:: |
| kReportWithoutResponseDetails; |
| } |
| |
| navigation_or_document_handle_ = |
| NavigationOrDocumentHandle::CreateForNavigation(*this); |
| |
| policy_container_builder_.emplace( |
| GetParentFrame(), |
| initiator_frame_token_.has_value() ? &*initiator_frame_token_ : nullptr, |
| initiator_process_id_, GetStoragePartitionWithCurrentSiteInfo(), |
| frame_entry); |
| |
| NavigationControllerImpl* controller = GetNavigationController(); |
| |
| if (frame_entry) { |
| frame_entry_item_sequence_number_ = frame_entry->item_sequence_number(); |
| frame_entry_document_sequence_number_ = |
| frame_entry->document_sequence_number(); |
| } |
| |
| // Sanitize the referrer. |
| common_params_->referrer = Referrer::SanitizeForRequest( |
| common_params_->url, *common_params_->referrer); |
| |
| if (IsInPrimaryMainFrame()) { |
| loading_mem_tracker_ = PeakGpuMemoryTrackerFactory::Create( |
| viz::PeakGpuMemoryTracker::Usage::PAGE_LOAD); |
| } |
| |
| if (frame_tree_node_->IsInFencedFrameTree()) { |
| commit_params_->frame_policy.sandbox_flags |= |
| blink::kFencedFrameForcedSandboxFlags; |
| } |
| |
| if (base::FeatureList::IsEnabled(network::features::kSharedStorageAPI)) { |
| shared_storage_writable_opted_in_ = |
| frame_tree_node_->shared_storage_writable_opted_in(); |
| shared_storage_writable_eligible_ = |
| IsSharedStorageWritableEligibleForNavigationRequest( |
| frame_tree_node_, common_params_->url); |
| } |
| |
| if (from_begin_navigation_) { |
| // This is needed to commit data: and about: URLs in the same SiteInstance |
| // as the initiator frame. Note that this may not necessarily match the |
| // SiteInstance of the RenderFrameHost that sent the BeginNavigation IPC |
| // (i.e., `frame_tree_node_->current_frame_host()->GetSiteInstance()`) with |
| // SiteInstanceGroups, because some other frame in a different SiteInstance |
| // but same SiteInstanceGroup could've initiated this navigation. |
| source_site_instance_ = |
| RenderFrameHostImpl::GetSourceSiteInstanceFromFrameToken( |
| base::OptionalToPtr(GetInitiatorFrameToken()), |
| GetInitiatorProcessId(), |
| frame_tree_node_->current_frame_host()->GetStoragePartition()); |
| |
| // If the lookup above failed (e.g., when no initiator frame token was |
| // provided), fall back to the navigating frame's current SiteInstance. This |
| // ensures that this renderer-initiated navigation still has a valid source |
| // SiteInstance corresponding to the renderer process that initiated the |
| // navigation, which is needed for certain security checks based on |
| // `source_site_instance_`, such as the CanRequestURL() check in |
| // `OnRequestRedirected()`. |
| if (!source_site_instance_) { |
| source_site_instance_ = |
| frame_tree_node_->current_frame_host()->GetSiteInstance(); |
| } |
| |
| DCHECK(navigation_client.is_valid()); |
| SetNavigationClient(std::move(navigation_client)); |
| |
| // Wait for renderer-initiated cancellation if needed. Navigation can |
| // proceed as soon as the corresponding JS task in the renderer finishes |
| // without calling window.stop() or other navigation cancellation triggers. |
| // That means there is no need to synchronise this signal with other |
| // renderer events, so this interface doesn't have to be associated and can |
| // use a prioritized task runner. |
| // kNavigationNetworkResponse is used as CommitNavigation typically already |
| // runs in on a task from this task runner (via OnResponseReceived message |
| // received from the network service). |
| if (renderer_cancellation_listener.is_valid()) { |
| renderer_cancellation_listener_.Bind( |
| std::move(renderer_cancellation_listener), |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| } |
| } else if (entry) { |
| DCHECK(!navigation_client.is_valid()); |
| if (frame_entry) { |
| source_site_instance_ = frame_entry->source_site_instance(); |
| dest_site_instance_ = frame_entry->site_instance(); |
| bindings_ = frame_entry->bindings(); |
| |
| // Handle navigations that require a |source_site_instance| but do not |
| // have one set yet. This can happen when navigation entries are restored |
| // from PageState objects, because the serialized state does not contain a |
| // SiteInstance so we need to use the |initiator_origin| to get an |
| // appropriate source SiteInstance. This can also happen when the |
| // |source_site_instance| was suppressed because of navigating in |
| // "noopener" mode. |
| // |
| // History subframe, restore navigations and |was_opener_suppressed| are |
| // the only cases where SetSourceSiteInstanceToInitiatorIfNeeded needs to |
| // be called (i.e. the only cases that may have no |source_site_instance_| |
| // even though RequiresInitiatorBasedSourceSiteInstance returns true). We |
| // verify that other cases which require a |source_site_instance_| indeed |
| // have one with a DCHECK below. |
| if (common_params_->is_history_navigation_in_new_child_frame || |
| common_params_->navigation_type == |
| blink::mojom::NavigationType::RESTORE || |
| common_params_->navigation_type == |
| blink::mojom::NavigationType::RESTORE_WITH_POST || |
| was_opener_suppressed) { |
| SetSourceSiteInstanceToInitiatorIfNeeded(); |
| } |
| } |
| isolation_info_ = entry->isolation_info(); |
| |
| // Ensure that we always have a |source_site_instance_| for navigations |
| // that require it at this point. This is needed to ensure that data: URLs |
| // commit in the SiteInstance that initiated them. |
| // |
| // TODO(acolwell): Move this below so it can be enforced on all paths. |
| // This requires auditing same-document and other navigations that don't |
| // have |from_begin_navigation_| or |entry| set. |
| DCHECK(!RequiresInitiatorBasedSourceSiteInstance() || |
| source_site_instance_); |
| } |
| |
| // Let the NTP override the navigation params and pretend that this is a |
| // browser-initiated, bookmark-like navigation. |
| // TODO(crbug.com/40702467): determine why some link navigations on chrome:// |
| // pages have |browser_initiated| set to true and others set to false. |
| if (source_site_instance_) { |
| bool is_renderer_initiated = !browser_initiated; |
| Referrer referrer(*common_params_->referrer); |
| ui::PageTransition transition = |
| ui::PageTransitionFromInt(common_params_->transition); |
| // TODO(crbug.com/388998723): Avoid unintentionally creating a process for |
| // source_site_instance_ if it doesn't have one. |
| GetContentClient()->browser()->OverrideNavigationParams( |
| source_site_instance_ |
| ->GetOrCreateProcess(ProcessAllocationContext{ |
| ProcessAllocationSource::kOverrideNavigationParams}) |
| ->GetProcessLock() |
| .site_url(), |
| &transition, &is_renderer_initiated, &referrer, |
| &common_params_->initiator_origin); |
| common_params_->transition = transition; |
| common_params_->referrer = |
| blink::mojom::Referrer::New(referrer.url, referrer.policy); |
| browser_initiated = !is_renderer_initiated; |
| } |
| commit_params_->is_browser_initiated = browser_initiated; |
| |
| // Update the load flags with cache information. |
| UpdateLoadFlagsWithCacheFlags(&begin_params_->load_flags, |
| common_params_->navigation_type, |
| common_params_->method == "POST"); |
| |
| // Add necessary headers that may not be present in the |
| // blink::mojom::BeginNavigationParams. |
| if (entry) { |
| // TODO(altimin, crbug.com/933147): Remove this logic after we are done |
| // with implementing back-forward cache. |
| if (frame_tree_node->IsOutermostMainFrame() && |
| entry->back_forward_cache_metrics()) { |
| entry->back_forward_cache_metrics() |
| ->MainFrameDidStartNavigationToDocument(); |
| } |
| |
| // If this NavigationRequest is for the current pending entry, make sure |
| // that we will discard the pending entry if all of associated its requests |
| // go away, by creating a ref to it. |
| if (entry == controller->GetPendingEntry()) |
| pending_entry_ref_ = controller->ReferencePendingEntry(); |
| |
| // |commit_params->is_overriding_user_agent| is the single source of truth |
| // in NavigationRequest. For history navigations, callers of this |
| // constructor must not provide conflicting requirements. Only |
| // |commit_params->is_overriding_user_agent| will be taken into account. |
| DCHECK_EQ(is_overriding_user_agent(), entry->GetIsOverridingUserAgent()); |
| } |
| |
| net::HttpRequestHeaders headers; |
| // Only add specific headers when creating a NavigationRequest before the |
| // network request is made, not at commit time. |
| if (!is_synchronous_renderer_commit) { |
| BrowserContext* browser_context = controller->GetBrowserContext(); |
| ClientHintsControllerDelegate* client_hints_delegate = |
| browser_context->GetClientHintsControllerDelegate(); |
| // Loading an about:srcdoc url on the main frame will cause a failure later |
| // and GetTentativeOriginAtRequestTime() can't handle it until then. |
| if ((CheckAboutSrcDoc() != AboutSrcDocCheckResult::BLOCK_REQUEST) && |
| client_hints_delegate) { |
| net::HttpRequestHeaders client_hints_headers; |
| AddNavigationRequestClientHintsHeaders( |
| GetTentativeOriginAtRequestTime(), &client_hints_headers, |
| browser_context, client_hints_delegate, is_overriding_user_agent(), |
| frame_tree_node_, commit_params_->frame_policy.container_policy, |
| common_params_->url); |
| headers.MergeFrom(client_hints_headers); |
| } |
| |
| // Add reduced accept language header. |
| if (auto reduce_accept_lang_utils = |
| ReduceAcceptLanguageUtils::Create(browser_context); |
| reduce_accept_lang_utils && !devtools_accept_language_override_ && |
| !ReduceAcceptLanguageUtils::CheckDisableReduceAcceptLanguageOriginTrial( |
| common_params_->url, frame_tree_node_, |
| browser_context->GetOriginTrialsControllerDelegate())) { |
| // Add the Accept-Language header with the reduce accept language value. |
| // Chromium network stack won't overwrite the value if Accept-Language |
| // header was already added in the request header. |
| net::HttpRequestHeaders accept_language_headers; |
| std::optional<std::string> reduced_accept_language = |
| reduce_accept_lang_utils.value() |
| .AddNavigationRequestAcceptLanguageHeaders( |
| url::Origin::Create(common_params_->url), frame_tree_node_, |
| &accept_language_headers); |
| commit_params_->reduced_accept_language = |
| reduced_accept_language.value_or(""); |
| headers.MergeFrom(accept_language_headers); |
| } |
| |
| headers.AddHeadersFromString(begin_params_->headers); |
| AddAdditionalRequestHeaders( |
| &headers, common_params_->url, common_params_->navigation_type, |
| ui::PageTransitionFromInt(common_params_->transition), |
| controller->GetBrowserContext(), common_params_->method, |
| GetUserAgentOverride(), common_params_->initiator_origin, |
| common_params_->referrer.get(), frame_tree_node); |
| |
| if (begin_params_->is_form_submission) { |
| // During form resubmit, `commit_params_->post_content_type` is populated |
| // from the history. Use it. |
| if (!commit_params_->post_content_type.empty()) { |
| headers.SetHeaderIfMissing(net::HttpRequestHeaders::kContentType, |
| commit_params_->post_content_type); |
| } |
| |
| // Save the Content-Type in case the form is resubmitted. This will get |
| // sent back to the renderer in the CommitNavigation IPC. The renderer |
| // will then send it back with the post body so that we can access it |
| // along with the body in FrameNavigationEntry::page_state_. |
| if (std::optional<std::string> content_type = |
| headers.GetHeader(net::HttpRequestHeaders::kContentType); |
| content_type) { |
| commit_params_->post_content_type = std::move(content_type).value(); |
| } |
| } |
| |
| TopicsHeaderValueResult topics_header_value_result = |
| GetTopicsHeaderValueForNavigationRequest(frame_tree_node, |
| common_params_->url); |
| |
| topics_eligible_ = topics_header_value_result.topics_eligible; |
| |
| if (topics_header_value_result.header_value) { |
| headers.SetHeader(kBrowsingTopicsRequestHeaderKey, |
| *topics_header_value_result.header_value); |
| } |
| |
| if (has_ad_auction_headers_attribute_ && |
| IsAdAuctionHeadersEligibleForNavigation( |
| *frame_tree_node_, url::Origin::Create(common_params_->url))) { |
| ad_auction_headers_eligible_ = true; |
| headers.SetHeader(kAdAuctionRequestHeaderKey, "?1"); |
| } |
| } |
| |
| begin_params_->headers = headers.ToString(); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| RenderWidgetHostImpl* host = RenderWidgetHostImpl::From( |
| frame_tree_node_->current_frame_host()->GetRenderWidgetHost()); |
| if (NeedsUrlLoader() && IsInPrimaryMainFrame() && host && |
| !host->is_hidden() && host->GetView() && |
| host->GetView()->GetNativeView() && |
| host->GetView()->GetNativeView()->GetWindowAndroid()) { |
| // If the compositor changes, we will just let the lock timeout instead of |
| // trying to deal with it explicitly. |
| ui::WindowAndroidCompositor* compositor = |
| host->GetView()->GetNativeView()->GetWindowAndroid()->GetCompositor(); |
| if (compositor) { |
| compositor_lock_ = compositor->GetCompositorLock(kCompositorLockTimeout); |
| } |
| } |
| |
| navigation_handle_proxy_ = std::make_unique<NavigationHandleProxy>(this); |
| #endif |
| |
| if (NeedsUrlLoader() && common_params_->url.SchemeIsHTTPOrHTTPS()) { |
| if (GetContentClient()->browser()->ShouldPreconnectNavigation( |
| frame_tree_node_->current_frame_host())) { |
| auto* storage_partition = |
| frame_tree_node_->current_frame_host()->GetStoragePartition(); |
| storage_partition->GetNetworkContext()->PreconnectSockets( |
| 1, common_params_->url, network::mojom::CredentialsMode::kInclude, |
| GetIsolationInfo().network_anonymization_key(), |
| net::MutableNetworkTrafficAnnotationTag(), |
| /*keepalive_config=*/std::nullopt, mojo::NullRemote()); |
| } |
| } |
| |
| if (NeedsUrlLoader() && IsInOutermostMainFrame()) { |
| MaybePrewarmHttpDiskCache(*controller->GetBrowserContext(), GetURL(), |
| GetInitiatorOrigin()); |
| } |
| |
| // Checking OriginCanAccessServiceWorkers() is needed before calling |
| // GetTentativeOriginAtRequestTime() since loading an about:srcdoc URL |
| // on the main frame will cause a failure while processing |
| // GetTentativeOriginAtRequestTime(). |
| if (OriginCanAccessServiceWorkers(GetURL())) { |
| // Preflight request for FindRegistrationForClientUrl. This |
| // preflight request speeds-up the upcoming |
| // FindRegistrationForClientUrl requests because the upcoming |
| // requests will be merged into this preflight request in |
| // `ServiceWorkerRegistry::FindRegistrationForClientUrl()` and |
| // `ServiceWorkerRegistry::RunFindRegistrationCallbacks()` later. |
| if (ServiceWorkerContext* context = |
| frame_tree_node_->navigator() |
| .controller() |
| .GetBrowserContext() |
| ->GetStoragePartition(site_info_.storage_partition_config()) |
| ->GetServiceWorkerContext()) { |
| const blink::StorageKey key = blink::StorageKey::CreateFirstParty( |
| GetTentativeOriginAtRequestTime()); |
| if (context->MaybeHasRegistrationForStorageKey(key)) { |
| // `CheckHasServiceWorker` calls `FindRegistrationForClientUrl` |
| // internally. |
| context->CheckHasServiceWorker(GetURL(), key, base::DoNothing()); |
| } |
| } |
| } |
| |
| // Only update the BackForwardCacheMetrics if this is for a navigation that |
| // could've been served from the bfcache. |
| if (IsBackForwardCacheEnabled() && !IsServedFromBackForwardCache() && entry && |
| BackForwardCacheMetrics::IsCrossDocumentMainFrameHistoryNavigation( |
| this)) { |
| // Update NotRestoredReasons and create a metrics object if there's none. |
| entry->UpdateBackForwardCacheNotRestoredReasons(this); |
| auto* metrics = entry->back_forward_cache_metrics(); |
| DCHECK(metrics); |
| if (base::FeatureList::IsEnabled( |
| blink::features::kBackForwardCacheSendNotRestoredReasons)) { |
| // Only populate the web-exposed NotRestoredReasons when needed by |
| // the NotRestoredReasons API. |
| commit_params_->not_restored_reasons = |
| metrics->GetWebExposedNotRestoredReasons(); |
| |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kBackForwardCacheUpdateNotRestoredReasonsName)) { |
| // Check that the reasons are not null since |this| is not served from |
| // back/forward cache. |
| // Session restored cases will be reported as null with the flag on and |
| // this check will no longer hold true. |
| CHECK(!commit_params_->not_restored_reasons.is_null()); |
| } |
| } |
| } |
| // Check that the reasons are null when |this| is served from back/forward |
| // cache. |
| if (base::FeatureList::IsEnabled( |
| blink::features::kBackForwardCacheSendNotRestoredReasons) && |
| IsBackForwardCacheEnabled() && IsServedFromBackForwardCache()) { |
| CHECK(commit_params_->not_restored_reasons.is_null()); |
| } |
| |
| // Record `SameDocumentCrossOriginInitiator` metric. It happens in the |
| // NavigationRequest constructor, to catch every kind of same-document |
| // navigation: the one initiated from the navigating frame's process, and the |
| // others. |
| if (common_params_->navigation_type == |
| blink::mojom::NavigationType::SAME_DOCUMENT && |
| GetInitiatorOrigin() && |
| !GetInitiatorOrigin()->IsSameOriginWith( |
| GetTentativeOriginAtRequestTime())) { |
| // This is reported to navigating frame's current document, because this is |
| // the document that behave differently if this navigation was turned into a |
| // cross-document one. |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| frame_tree_node_->current_frame_host(), |
| blink::mojom::WebFeature::kSameDocumentCrossOriginInitiator); |
| } |
| |
| if (!GetContentClient()->browser()->IsBrowserStartupComplete()) { |
| confidence_level_ = blink::mojom::ConfidenceLevel::kLow; |
| } |
| } |
| |
| NavigationRequest::~NavigationRequest() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::~NavigationRequest", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN); |
| #if DCHECK_IS_ON() |
| // If |is_safe_to_delete_| is false, it means |this| is being deleted at an |
| // unexpected time, more specifically a time that is likely to lead to |
| // crashing when the stack unwinds (use after free). The typical scenario for |
| // this is calling to the delegate when the delegate is not expected to make |
| // any sort of state change. For example, when the delegate is informed that a |
| // navigation has started the delegate is not expected to call Stop(). |
| DCHECK(is_safe_to_delete_); |
| #endif |
| |
| // Close the last child event. Passing nullptr as the event name will match |
| // the end event with the last unmatched begin event. |
| TRACE_EVENT_NESTABLE_ASYNC_END0("navigation", nullptr, navigation_id_); |
| TRACE_EVENT_NESTABLE_ASYNC_END0("navigation", "NavigationRequest", |
| navigation_id_); |
| |
| // IMPORTANT NOTE: DO NOT return early from the destructor before this line. |
| // Otherwise, a queued navigation might get stuck in a queueing state forever. |
| // This navigation has finished. See if there is another NavigationRequest |
| // that lives in the associated FrameTreeNode that satisfies these conditions: |
| // - Is currently queued to wait for a pending commit navigation to finish |
| // - Is not the NavigationRequest that is currently being destructed itself |
| if (NavigationRequest* request = frame_tree_node_->navigation_request()) { |
| if (request->IsQueued() && request != this) { |
| // It might be possible for the pending commit RFH to still exist, e.g. if |
| // the navigation being destructed is an unrelated navigation |
| // (same-document navigation etc). In that case, don't continue the queued |
| // navigation just yet. |
| if (!request->ShouldQueueDueToExistingPendingCommitRFH()) { |
| request->PostResumeCommitTask(); |
| } |
| } |
| } |
| |
| if (loading_mem_tracker_) |
| loading_mem_tracker_->Cancel(); |
| ResetExpectedProcess(); |
| |
| if (IsInPrimaryMainFrame()) { |
| if (auto* cache = |
| GetNavigationController()->GetNavigationEntryScreenshotCache()) { |
| cache->OnNavigationFinished(*this); |
| } |
| } |
| |
| if (HasCommitted()) { |
| CHECK(!navigation_discard_reason_.has_value()); |
| } else { |
| if (!navigation_discard_reason_.has_value()) { |
| // If a navigation is destructed without having committed, it should have |
| // a reason set. |
| // TODO(crbug.com/390569133): Change this back to a CHECK once we make |
| // sure this is never hit. |
| SCOPED_CRASH_KEY_NUMBER("Bug390569133", "navigation_state", (int)state_); |
| base::debug::DumpWithoutCrashing(); |
| } |
| |
| // If we're before WILL_START_NAVIGATION, we haven't reported request start |
| // to DevTools yet. |
| // If we're in WILL_FAIL_REQUEST, the failure has been reported already. |
| if (state_ >= WILL_START_NAVIGATION && state_ != WILL_FAIL_REQUEST) { |
| devtools_instrumentation::OnNavigationRequestFailed( |
| *this, network::URLLoaderCompletionStatus(net::ERR_ABORTED)); |
| } |
| |
| // NavigationRequests with pending Navigation API keys must notify the |
| // renderer when they fail. |
| if (pending_navigation_api_key_) { |
| frame_tree_node_->current_frame_host() |
| ->GetAssociatedLocalFrame() |
| ->TraverseCancelled( |
| *pending_navigation_api_key_, |
| blink::mojom::TraverseCancelledReason::kAbortedBeforeCommit); |
| } |
| |
| // If subframe history navigations were deferred waiting for this request, |
| // the cancelation of this request should cancel them, too. |
| for (auto& throttle : subframe_history_navigation_throttles_) { |
| if (throttle) { |
| throttle->Cancel(); |
| } |
| } |
| subframe_history_navigation_throttles_.clear(); |
| } |
| |
| // If this NavigationRequest is the last one referencing the pending |
| // NavigationEntry, the entry is discarded. |
| // |
| // Leaving a stale pending NavigationEntry with no matching navigation can |
| // potentially lead to URL-spoof issues. |
| // |
| // Note: Discarding the pending NavigationEntry is done before notifying the |
| // navigation finished to the observers. One class is relying on this: |
| // org.chromium.chrome.browser.toolbar.ToolbarManager |
| pending_entry_ref_.reset(); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (navigation_visible_to_embedder_) |
| navigation_handle_proxy_->DidFinish(); |
| #endif |
| |
| if (is_deferred_on_fenced_frame_url_mapping_) { |
| CHECK(NeedFencedFrameURLMapping()); |
| GetFencedFrameURLMap().RemoveObserverForURN(common_params_->url, this); |
| } |
| |
| RecordEarlyRenderFrameHostSwapMetrics(); |
| |
| if (IsNavigationStarted()) { |
| GetDelegate()->DidFinishNavigation(this); |
| ProcessOriginAgentClusterEndResult(); |
| if (IsInMainFrame()) { |
| TRACE_EVENT_NESTABLE_ASYNC_END2( |
| "navigation", "Navigation StartToCommit", |
| TRACE_ID_WITH_SCOPE("StartToCommit", TRACE_ID_LOCAL(this)), "URL", |
| common_params_->url.spec(), "Net Error Code", net_error_); |
| MaybeRecordTraceEventsAndHistograms(); |
| } |
| MaybeRecordNavigationStartAdjustments(); |
| |
| // Abandon the prerender host reserved for activation if it exists. |
| if (IsPrerenderedPageActivation()) { |
| GetPrerenderHostRegistry().OnActivationFinished( |
| prerender_frame_tree_node_id_.value()); |
| } |
| |
| if (!HasCommitted()) { |
| ResetViewTransitionState(); |
| } |
| |
| if (IsServedFromBackForwardCache()) { |
| auto bfcache_entry = |
| GetNavigationController()->GetBackForwardCache().GetOrEvictEntry( |
| nav_entry_id()); |
| if (!bfcache_entry.has_value() && |
| bfcache_entry.error() == |
| BackForwardCacheImpl::kEntryIneligibleAndEvicted) { |
| // DO NOT ADD CODE after this. When BFCache entry is evicted, the |
| // current NavigationRequest has been destroyed. |
| return; |
| } |
| if (bfcache_entry.has_value()) { |
| RenderFrameHostImpl* rfh = RenderFrameHostImpl::FromID( |
| bfcache_entry.value()->render_frame_host()->GetGlobalId()); |
| // RFH could have been deleted. E.g. eviction timer fired |
| if (rfh && rfh->IsInBackForwardCache()) { |
| // rfh is still in the cache so the navigation must have failed. But |
| // we have already disabled eviction so the safest thing to do here to |
| // recover is to evict. |
| // TODO(crbug.com/40283427): Only evict BFCache if the |
| // `BackForwardCacheCommitDeferringCondition`, which unfreezes the |
| // page and disables the eviction on the renderer side, is completed. |
| rfh->EvictFromBackForwardCacheWithReason( |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kNavigationCancelledWhileRestoring); |
| } |
| } |
| } |
| } else { |
| GetDelegate()->DidCancelNavigationBeforeStart(this); |
| } |
| } |
| |
| void NavigationRequest::RegisterCommitDeferringConditionForTesting( |
| std::unique_ptr<CommitDeferringCondition> condition) { |
| commit_deferrer_->AddConditionForTesting(std::move(condition)); // IN-TEST |
| } |
| |
| void NavigationRequest::SetCookieAccessObserversForTesting( |
| std::unique_ptr<CookieAccessObservers> observers) { |
| cookie_observers_ = std::move(observers); |
| } |
| |
| bool NavigationRequest::IsCommitDeferringConditionDeferredForTesting() { |
| if (!commit_deferrer_) |
| return false; |
| return commit_deferrer_->GetDeferringConditionForTesting(); // IN-TEST |
| } |
| |
| CommitDeferringCondition* |
| NavigationRequest::GetCommitDeferringConditionForTesting() { |
| if (!commit_deferrer_) |
| return nullptr; |
| return commit_deferrer_->GetDeferringConditionForTesting(); // IN-TEST |
| } |
| |
| void NavigationRequest::BeginNavigation() { |
| begin_navigation_time_ = base::TimeTicks::Now(); |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::BeginNavigation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("BeginNavigation", this); |
| DCHECK(!loader_); |
| DCHECK(!HasRenderFrameHost()); |
| ScopedCrashKeys crash_keys(*this); |
| |
| if (begin_navigation_callback_for_testing_) { |
| std::move(begin_navigation_callback_for_testing_).Run(); |
| } |
| |
| if (MaybeStartPrerenderingActivationChecks()) { |
| // BeginNavigationImpl() will be called after the checks. |
| return; |
| } |
| |
| MaybeAssignInvalidPrerenderFrameTreeNodeId(); |
| |
| // Fenced frames are not allowed to load if nested in iframes with CSPEE. |
| bool is_fenced_frame = frame_tree_node_->IsFencedFrameRoot(); |
| if (is_fenced_frame) { |
| DCHECK(!frame_tree_node_->csp_attribute()); |
| if (GetParentFrameOrOuterDocument()->required_csp()) { |
| GURL sanitized_blocked_url = |
| common_params_->url.DeprecatedGetOriginAsURL(); |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf( |
| "Refused to frame '%s' as a fenced frame because " |
| "CSP Embedded Enforcement is specified by the embedder", |
| sanitized_blocked_url.spec().c_str())); |
| |
| StartNavigation(); |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_BLOCKED_BY_CSP), |
| false /*skip_throttles*/, std::nullopt /*error_page_content*/, |
| false /*collapse_frame*/); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| } |
| |
| // If this is a fenced frame with a urn:uuid, or an iframe with a urn::uuid |
| // given blink::features::kAllowURNsInIframes is enabled, then convert it to a |
| // url before starting the navigation; otherwise, proceed directly with the |
| // navigation. |
| // In long term, navigation support for urn::uuid in iframes will be |
| // deprecated. Currently we issue a console warning when navigation starts. |
| // TODO(crbug.com/40060657) |
| if (NeedFencedFrameURLMapping()) { |
| if (!is_fenced_frame) { |
| // Iframes with urn::uuid. |
| DCHECK(!frame_tree_node_->IsMainFrame()); |
| DCHECK(blink::features::IsAllowURNsInIframeEnabled()); |
| if (blink::features::DisplayWarningDeprecateURNIframesUseFencedFrames()) { |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| "Protected Audience/selectURL will deprecate supporting iframes to " |
| "render the winning ad/selected URL. " |
| "Please use fenced frames instead. See " |
| "https://developer.chrome.com/en/docs/privacy-sandbox/fenced-frame/" |
| "#examples"); |
| } |
| } |
| |
| UMA_HISTOGRAM_BOOLEAN( |
| "Navigation.BrowserMappedUrnUuidInIframeOrFencedFrame", |
| !is_fenced_frame); |
| |
| FencedFrameURLMapping& fenced_frame_urls_map = GetFencedFrameURLMap(); |
| |
| // If the mapping finishes synchronously, OnFencedFrameURLMappingComplete |
| // will be synchronously called and will reset |
| // `is_deferred_on_fenced_frame_url_mapping_` to false. |
| is_deferred_on_fenced_frame_url_mapping_ = true; |
| |
| fenced_frame_url_mapping_start_time_ = base::TimeTicks::Now(); |
| |
| // OnFencedFrameURLMappingComplete() and BeginNavigationImpl() will be |
| // invoked after this. |
| fenced_frame_urls_map.ConvertFencedFrameURNToURL(common_params_->url, |
| /*observer=*/this); |
| // DO NOT ADD CODE after this. The previous call to |
| // ConvertFencedFrameURNToURL may cause the destruction of the |
| // NavigationRequest. |
| return; |
| } |
| |
| // Send any potential navigation start automatic beacons for this frame. |
| frame_tree_node_->current_frame_host() |
| ->MaybeSendFencedFrameAutomaticReportingBeacon( |
| *this, blink::mojom::AutomaticBeaconType::kTopNavigationStart); |
| |
| // Log a histogram for a top-level navigation that initiates from a fenced |
| // frame or URN iframe. |
| if (GetInitiatorDocumentRenderFrameHost() && |
| GetInitiatorDocumentRenderFrameHost() |
| ->frame_tree_node() |
| ->GetFencedFrameProperties() |
| .has_value() && |
| IsInOutermostMainFrame()) { |
| base::UmaHistogramEnumeration(blink::kFencedFrameTopNavigationHistogram, |
| blink::FencedFrameNavigationState::kBegin); |
| } |
| |
| BeginNavigationImpl(); |
| } |
| |
| void NavigationRequest::UpdateNavigationStartTime(const base::TimeTicks& time, |
| bool for_legacy, |
| bool showed_dialog) { |
| // Track the adjustment details for https://crbug.com/385170155. |
| // Note: It is possible to get here more than once for a single request, which |
| // might happen if a beforeunload ack for a different navigation is received |
| // at the wrong time (see https://crbug.com/402545469). In that case, preserve |
| // the existing `original_navigation_start_`. |
| // TODO(crbug.com/404286908): Track which NavigationRequest should be updated |
| // in response to a given beforeunload completion. |
| if (original_navigation_start_.is_null()) { |
| original_navigation_start_ = common_params_->navigation_start; |
| } |
| navigation_start_adjustment_for_legacy_ = for_legacy; |
| beforeunload_dialog_shown_ = showed_dialog; |
| |
| common_params_->navigation_start = time; |
| |
| if (for_legacy) { |
| // Legacy PostTasks do not run any actual beforeunload handlers and should |
| // be treated as part of navigation overhead, rather than excluded. |
| // |
| // Note: Due to crbug.com/404286908, it is possible for this function to be |
| // called with data from an earlier NavigationRequest that has been canceled |
| // before this request started, and thus `for_legacy` may not be accurate. |
| // Until that bug is fixed, though, it is still accurate to treat the |
| // current request as legacy (even if it meant to run actual beforeunload |
| // handlers) because the navigation will proceed anyway without waiting for |
| // those handlers. |
| beforeunload_phase2_start_time_ = base::TimeTicks(); |
| beforeunload_phase2_end_time_ = base::TimeTicks(); |
| } else { |
| // Non-legacy cases that ran beforeunload handlers should set an end time |
| // for BeforeUnload phase 2 if there was a corresponding start time. This |
| // end time should be close to the end of running beforeunload. |
| // |
| // Note: Until crbug.com/404286908 is fixed, it is possible for `time` to be |
| // before the start time, due to updating the wrong NavigationRequest. In |
| // that rare case, use the current time instead to ensure the interval is |
| // well-defined (even if that causes us to ignore a little more time than |
| // necessary by making BeforeUnload phase 2 look a little longer). Also |
| // avoid changing the end time once it is set, in case this is called more |
| // than once. |
| if (!beforeunload_phase2_start_time_.is_null() && |
| beforeunload_phase2_end_time_.is_null()) { |
| beforeunload_phase2_end_time_ = (beforeunload_phase2_start_time_ <= time) |
| ? time |
| : base::TimeTicks::Now(); |
| } |
| } |
| } |
| |
| bool NavigationRequest::MaybeStartPrerenderingActivationChecks() { |
| // Find an available prerendered page for this request. If it's found, this |
| // request may activate it instead of loading a page via network. |
| FrameTreeNodeId candidate_prerender_frame_tree_node_id = |
| GetPrerenderHostRegistry().FindPotentialHostToActivate(*this); |
| if (candidate_prerender_frame_tree_node_id.is_null()) { |
| return false; |
| } |
| |
| // Run CommitDeferringConditions before activating the prerendered page. See |
| // the comemnt on RunCommitDeferringConditions() for details. |
| // |
| // The prerendered page can be destroyed while the conditions are running. |
| // In that case, this request gives up activating it and instead falls back to |
| // a regular navigation. |
| commit_deferrer_ = CommitDeferringConditionRunner::Create( |
| *this, |
| CommitDeferringCondition::NavigationType::kPrerenderedPageActivation, |
| candidate_prerender_frame_tree_node_id); |
| is_running_potential_prerender_activation_checks_ = true; |
| |
| // Post a task to run the conditions in case BeginNavigation() is not expected |
| // to run synchronously. OnPrerenderingActivationChecksComplete() will be |
| // called after all the deferring conditions finish. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostNonNestableTask( |
| FROM_HERE, |
| base::BindOnce(&NavigationRequest::RunCommitDeferringConditions, |
| weak_factory_.GetWeakPtr())); |
| return true; |
| } |
| |
| void NavigationRequest::OnPrerenderingActivationChecksComplete( |
| CommitDeferringCondition::NavigationType navigation_type, |
| std::optional<FrameTreeNodeId> candidate_prerender_frame_tree_node_id) { |
| TRACE_EVENT_WITH_FLOW0( |
| "navigation", "NavigationRequest::OnPrerenderingActivationChecksComplete", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| // Prerendered page activation must run CommitDeferringConditions before |
| // StartRequest(). |
| DCHECK_LT(state_, WILL_START_NAVIGATION); |
| |
| DCHECK(candidate_prerender_frame_tree_node_id.has_value()); |
| DCHECK(!prerender_frame_tree_node_id_.has_value()); |
| |
| // Attempt to reserve the potential PrerenderHost. |
| // |
| // If it has been requested to cancel prerendered page activation during |
| // CommitDeferringConditions, ReserveHostToActivate() returns an invalid |
| // FrameTreeNodeId, and then NavigationRequest continues as regular |
| // navigation. |
| prerender_frame_tree_node_id_ = |
| GetPrerenderHostRegistry().ReserveHostToActivate( |
| *this, candidate_prerender_frame_tree_node_id.value()); |
| if (prerender_frame_tree_node_id_.value().is_null()) { |
| // If we ran commit deferring conditions for a potential pre-render which |
| // eventually wasn't activated, abort the ViewTransition. The state was |
| // cached assuming this navigation will be same-origin which might not be |
| // the case now that we need to make a network request. |
| ResetViewTransitionState(); |
| } else { |
| // The reserved host should match with the potential host. Otherwise the |
| // reserved host may not be ready for activation yet as we haven't run |
| // PrerenderCommitDeferringCondition for the host to finish navigation in |
| // the prerendering main frame. |
| DCHECK_EQ(prerender_frame_tree_node_id_.value(), |
| candidate_prerender_frame_tree_node_id.value()); |
| } |
| is_running_potential_prerender_activation_checks_ = false; |
| commit_deferrer_.reset(); |
| |
| // We can only activate top-level pages, which can never be at a fenced frame |
| // URN that needs to be mapped. |
| CHECK(!NeedFencedFrameURLMapping()); |
| |
| BeginNavigationImpl(); |
| // DO NOT ADD CODE after this. The previous call to |
| // BeginNavigationImpl may cause the destruction of the NavigationRequest. |
| } |
| |
| FencedFrameURLMapping& NavigationRequest::GetFencedFrameURLMap() { |
| // The usual case here is a fenced frame root navigating to a URNs, in which |
| // case we need to consult the `FencedFrameURLMapping` in the *outer* |
| // FrameTree. |
| bool is_fenced_frame_root = |
| frame_tree_node_->current_frame_host()->IsFencedFrameRoot(); |
| FrameTreeNode* node_to_use = frame_tree_node_->frame_tree() |
| .root() |
| ->render_manager() |
| ->GetOuterDelegateNode(); |
| |
| // However the very unusual case is an *iframe* (that supports navigations to |
| // URNs via `blink::features::IsAllowURNsInIframeEnabled`) navigating to a |
| // URN, possibly *inside* of a fenced frame. We can remove support for this |
| // case once third party cookies are removed. |
| if (!is_fenced_frame_root) { |
| node_to_use = frame_tree_node_; |
| } |
| DCHECK(node_to_use); |
| return node_to_use->current_frame_host()->GetPage().fenced_frame_urls_map(); |
| } |
| |
| bool NavigationRequest::NeedFencedFrameURLMapping() { |
| if (!blink::IsValidUrnUuidURL(common_params_->url)) { |
| return false; |
| } |
| if (blink::features::IsAllowURNsInIframeEnabled() && |
| !frame_tree_node_->IsMainFrame() && |
| !frame_tree_node_->IsFencedFrameRoot()) { |
| // When urn iframes are enabled, any urn:uuid navigation to an iframe is |
| // resolved using the urn mapping. |
| is_embedder_initiated_fenced_frame_navigation_ = true; |
| } |
| return is_embedder_initiated_fenced_frame_navigation_; |
| } |
| |
| void NavigationRequest::OnFencedFrameURLMappingComplete( |
| const std::optional<FencedFrameProperties>& properties) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::OnFencedFrameURLMappingComplete", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| is_deferred_on_fenced_frame_url_mapping_ = false; |
| |
| // The URL mapping might have failed (e.g. because the urn is invalid): |
| if (!properties.has_value()) { |
| // For iframes, try the urn as-is to maintain existing behavior which will |
| // abort the navigation as the url is unresolvable. |
| if (!frame_tree_node_->IsFencedFrameRoot()) { |
| BeginNavigationImpl(); // DO NOT ADD CODE after this, because it might |
| // have destroyed `this`. |
| return; |
| } |
| |
| StartNavigation(); |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_INVALID_URL), |
| false /* skip_throttles */, std::nullopt /* error_page_content*/, |
| false /* collapse_frame */); |
| return; |
| } |
| |
| if (properties->on_navigate_callback()) { |
| properties->on_navigate_callback().Run(); |
| } |
| |
| // Currently, all fenced frame use cases include mapped urls. Patch up |
| // url-related fields to use the underlying mapped url, rather than the |
| // original urn. |
| CHECK(properties->mapped_url().has_value()); |
| const GURL& mapped_url_value = |
| properties->mapped_url()->GetValueIgnoringVisibility(); |
| common_params_->url = mapped_url_value; |
| commit_params_->original_url = mapped_url_value; |
| |
| // Store the browser's view of the fenced frame properties along with any |
| // embedder context for shared storage in the`NavigationRequest`. Upon commit, |
| // it will be stored in the fenced frame root `FrameTreeNode`. |
| fenced_frame_properties_ = properties; |
| |
| // Set the shared storage context in the fenced frame properties. |
| DCHECK(fenced_frame_properties_); |
| fenced_frame_properties_->SetEmbedderSharedStorageContext( |
| embedder_shared_storage_context_); |
| embedder_shared_storage_context_ = std::nullopt; |
| |
| // For urns loaded into iframes, we disable certain aspects of fenced frames: |
| // * a storage/network partition nonce |
| // * the ability to call window.fence.disableUntrustedNetwork |
| if (!frame_tree_node_->IsFencedFrameRoot()) { |
| CHECK(blink::features::IsAllowURNsInIframeEnabled()); |
| fenced_frame_properties_->AdjustPropertiesForUrnIframe(); |
| } |
| |
| // This implies the URN is created from shared storage. |
| if (fenced_frame_properties_->shared_storage_budget_metadata()) { |
| base::TimeDelta time_spent_in_fenced_frame_url_mapping = |
| base::TimeTicks::Now() - fenced_frame_url_mapping_start_time_; |
| |
| base::UmaHistogramTimes( |
| "Storage.SharedStorage.Timing.UrlMappingDuringNavigation", |
| time_spent_in_fenced_frame_url_mapping); |
| } |
| |
| BeginNavigationImpl(); // DO NOT ADD CODE after this, because it might have |
| // destroyed `this`. |
| } |
| |
| void NavigationRequest::BeginNavigationImpl() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::BeginNavigationImpl", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| base::ElapsedTimer timer; |
| SetState(WILL_START_NAVIGATION); |
| #if BUILDFLAG(IS_ANDROID) |
| base::WeakPtr<NavigationRequest> this_ptr(weak_factory_.GetWeakPtr()); |
| bool should_override_url_loading = false; |
| |
| if (!GetContentClient()->browser()->ShouldOverrideUrlLoading( |
| frame_tree_node_->frame_tree_node_id(), |
| commit_params_->is_browser_initiated, commit_params_->original_url, |
| commit_params_->original_method, common_params_->has_user_gesture, |
| false, frame_tree_node_->IsOutermostMainFrame(), |
| frame_tree_node_->frame_tree().is_prerendering(), |
| ui::PageTransitionFromInt(common_params_->transition), |
| &should_override_url_loading)) { |
| if (prerender_frame_tree_node_id_.has_value() && |
| !prerender_frame_tree_node_id_.value().is_null()) { |
| // Prerender activation must not fail but some reports imply it can |
| // actually be failing: crbug.com/408969974. This dump is useful for |
| // debugging it. |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(); |
| std::string prerender_type = GeneratePrerenderHistogramSuffix( |
| registry.GetPrerenderTriggerType(prerender_frame_tree_node_id()), |
| registry.GetPrerenderEmbedderHistogramSuffix( |
| prerender_frame_tree_node_id())); |
| SCOPED_CRASH_KEY_STRING64("Bug411566699", "prerender_type", |
| prerender_type); |
| base::debug::DumpWithoutCrashing(); |
| } |
| |
| // A Java exception was thrown by the embedding application; we |
| // need to return from this task. Specifically, it's not safe from |
| // this point on to make any JNI calls. |
| return; |
| } |
| |
| // The content/ embedder might cause |this| to be deleted while |
| // |ShouldOverrideUrlLoading| is called. |
| // See https://crbug.com/770157. |
| if (!this_ptr) |
| return; |
| |
| if (should_override_url_loading) { |
| // Don't create a NavigationHandle here to simulate what happened with the |
| // old navigation code path (i.e. doesn't fire onPageFinished notification |
| // for aborted loads). |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = |
| static_cast<int32_t>(ErrorNavigationTrigger::kShouldOverrideUrlLoading); |
| OnRequestFailedInternal(completion_status, false /*skip_throttles*/, |
| std::nullopt /*error_page_content*/, |
| false /*collapse_frame*/); |
| return; |
| } |
| #endif |
| |
| // Check Content Security Policy before the NavigationThrottles run. This |
| // gives CSP a chance to modify requests that NavigationThrottles would |
| // otherwise block. Similarly, the NavigationHandle is created afterwards, so |
| // that it gets the request URL after potentially being modified by CSP. |
| net::Error net_error = CheckContentSecurityPolicy( |
| false /* has_followed redirect */, |
| false /* url_upgraded_after_redirect */, false /* is_response_check */); |
| if (net_error != net::OK) { |
| SCOPED_CRASH_KEY_NUMBER("NavReq", "net_error", static_cast<int>(net_error)); |
| SCOPED_CRASH_KEY_NUMBER("NavReq", "navigating_frame_type", |
| static_cast<int>(GetNavigatingFrameType())); |
| // Create a navigation handle so that the correct error code can be set on |
| // it by OnRequestFailedInternal(). |
| StartNavigation(); |
| OnRequestFailedInternal(network::URLLoaderCompletionStatus(net_error), |
| false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (CheckCredentialedSubresource() == |
| CredentialedSubresourceCheckResult::BLOCK_REQUEST) { |
| // Create a navigation handle so that the correct error code can be set on |
| // it by OnRequestFailedInternal(). |
| StartNavigation(); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kCredentialedSubresourceBlocked); |
| OnRequestFailedInternal(completion_status, false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| StartNavigation(); |
| |
| // The previous call to `StartNavigation()` could have changed the |
| // is_overriding_user_agent value in CommitNavigationParams. If we're trying |
| // to restore an entry from the back-forward cache, we need to ensure that |
| // the is_overriding_user_agent used in the RenderFrameHost to restore matches |
| // the value set in CommitNavigationParams. |
| if (IsServedFromBackForwardCache() && |
| GetRenderFrameHostRestoredFromBackForwardCache() |
| ->GetPage() |
| .is_overriding_user_agent() != |
| commit_params_->is_overriding_user_agent) { |
| // Trigger an eviction, which will cancel this navigation and trigger a new |
| // one to the same entry (but won't try to restore the entry from the |
| // back-forward cache) asynchronously. |
| GetRenderFrameHostRestoredFromBackForwardCache() |
| ->EvictFromBackForwardCacheWithReason( |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kUserAgentOverrideDiffers); |
| // DO NOT ADD CODE after this. The previous call to |
| // `EvictFromBackForwardCacheWithReason()` |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (CheckAboutSrcDoc() == AboutSrcDocCheckResult::BLOCK_REQUEST) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_INVALID_URL), |
| true /* skip_throttles */, std::nullopt /* error_page_content*/, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (browser_initiated_error_navigation_type_ != |
| BrowserInitiatedErrorNavigationType::kNone) { |
| OnRequestFailedInternal(network::URLLoaderCompletionStatus(net_error_), |
| true /* skip_throttles */, |
| error_page_html_ /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (IsForMhtmlSubframe()) |
| is_mhtml_or_subframe_ = true; |
| |
| // TODO(antoniosartori): This takes a snapshot of the 'csp' attribute. This |
| // should be done at the beginning of the navigation instead. Otherwise, the |
| // attribute might have change while waiting for the beforeunload handlers to |
| // complete. |
| SetupCSPEmbeddedEnforcement(); |
| |
| if (!NeedsUrlLoader()) { |
| // The types of pages that don't need a URL Loader should never get served |
| // from the BackForwardCache or activated from a prerender. |
| DCHECK(!IsServedFromBackForwardCache()); |
| DCHECK(!IsPrerenderedPageActivation()); |
| |
| // There is no need to make a network request for this navigation, so commit |
| // it immediately. |
| EnterChildTraceEvent("ResponseStarted", this); |
| |
| // |CheckCSPEmbeddedEnforcement()| below populates the |required_csp_|. No |
| // URLs will be blocked, because they are either: |
| // - allowing blanket enforcement of CSP (about:blank, about:srcdoc, ...). |
| // - MHTML document, not supported by CSPEE (https://crbug.com/1164353). |
| if (CheckCSPEmbeddedEnforcement() == |
| CSPEmbeddedEnforcementResult::BLOCK_RESPONSE) { |
| NOTREACHED(); |
| } |
| |
| ComputePoliciesToCommit(); |
| |
| // Same-document navigations occur in the currently loaded document. See |
| // also RenderFrameHostManager::DidCreateNavigationRequest() which will |
| // expect us to use the current RenderFrameHost for this NavigationRequest, |
| // and https://crbug.com/1125106. |
| if (IsSameDocument()) { |
| render_frame_host_ = frame_tree_node_->current_frame_host()->GetSafeRef(); |
| |
| // The SiteInstance should have a site already from the navigation that |
| // committed the document, unless the scheme does not require a site. Same |
| // document navigations cannot change scheme or origin, so it should be |
| // equivalent to check the current vs destination UrlInfo. |
| DCHECK(render_frame_host_.value()->GetSiteInstance()->HasSite() || |
| !SiteInstanceImpl::ShouldAssignSiteForUrlInfo(GetUrlInfo())); |
| |
| WillCommitWithoutUrlLoader(); |
| return; |
| } else { |
| // [spec]: https://html.spec.whatwg.org/C/#process-a-navigate-response |
| // 4. if [...] the result of checking a navigation response's adherence to |
| // its embedder policy [...], then set failure to true. |
| if (!CheckResponseAdherenceToCoep(common_params_->url)) { |
| OnRequestFailedInternal(network::URLLoaderCompletionStatus( |
| network::mojom::BlockedByResponseReason:: |
| kCoepFrameResourceNeedsCoepHeader), |
| false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| return; |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| } |
| |
| // Enforce cross-origin-opener-policy for about:blank, about:srcdoc and |
| // MHTML iframe, before selecting the RenderFrameHost. |
| const url::Origin origin = GetOriginForURLLoaderFactoryUnchecked(); |
| const net::SchemefulSite site = net::SchemefulSite(origin); |
| |
| coop_status_.EnforceCOOP( |
| policy_container_builder_->FinalPolicies().cross_origin_opener_policy, |
| origin, net::NetworkAnonymizationKey::CreateSameSite(site)); |
| |
| SelectFrameHostForCrossDocumentNavigationWithNoUrlLoader(); |
| return; |
| } |
| } |
| |
| base::UmaHistogramTimes( |
| base::StrCat({"Navigation.BeginNavigationImpl.", |
| IsInMainFrame() ? "MainFrame" : "Subframe"}), |
| timer.Elapsed()); |
| WillStartRequest(); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest:: |
| SelectFrameHostForCrossDocumentNavigationWithNoUrlLoader() { |
| DCHECK(!NeedsUrlLoader()); |
| CHECK(!HasRenderFrameHost()) |
| << "`render_frame_host_` should not be set before the " |
| "`NavigationRequest` starts to select the RFH."; |
| |
| if (auto result = |
| frame_tree_node_->render_manager()->GetFrameHostForNavigation( |
| this, &browsing_context_group_swap_, |
| ProcessAllocationContext::CreateForNavigationRequest( |
| ProcessAllocationNavigationStage::kNoURLLoader, |
| navigation_id_)); |
| result.has_value()) { |
| render_frame_host_ = result.value()->GetSafeRef(); |
| } else { |
| switch (result.error()) { |
| case GetFrameHostForNavigationFailed::kCouldNotReinitializeMainFrame: |
| // TODO(crbug.com/40250311): This was unhandled before and |
| // remains explicitly unhandled. This branch may be removed in the |
| // future. |
| break; |
| case GetFrameHostForNavigationFailed::kBlockedByPendingCommit: |
| resume_commit_closure_ = base::BindOnce( |
| &NavigationRequest:: |
| SelectFrameHostForCrossDocumentNavigationWithNoUrlLoader, |
| weak_factory_.GetWeakPtr()); |
| frame_tree_node_->render_manager() |
| ->speculative_frame_host() |
| ->RecordMetricsForBlockedGetFrameHostAttempt( |
| /* commit_attempt=*/true); |
| return; |
| } |
| } |
| |
| CHECK(Navigator::CheckWebUIRendererDoesNotDisplayNormalURL( |
| &*render_frame_host_.value(), GetUrlInfo(), |
| /*is_renderer_initiated_check=*/false)); |
| |
| auto* site_instance = render_frame_host_.value()->GetSiteInstance(); |
| if (!site_instance->HasSite() && |
| SiteInstanceImpl::ShouldAssignSiteForUrlInfo(GetUrlInfo())) { |
| site_instance->ConvertToDefaultOrSetSite(GetUrlInfo()); |
| } |
| |
| WillCommitWithoutUrlLoader(); |
| } |
| |
| void NavigationRequest::SetWaitingForRendererResponse() { |
| EnterChildTraceEvent("WaitingForRendererResponse", this); |
| SetState(WAITING_FOR_RENDERER_RESPONSE); |
| } |
| |
| bool NavigationRequest::ShouldAddCookieChangeListener() { |
| // The `CookieChangeListener` will only be set up if all of these are true: |
| // (1) the navigation's protocol is HTTP(s). |
| // (2) we allow a document with `Cache-control: no-store` header to |
| // enter back/forward. |
| // (3) the navigation is neither a same-document navigation nor a page |
| // activation, since in these cases, an existing `RenderFrameHost` will be |
| // used, and it would already have an existing listener, so we should skip the |
| // initialization. |
| // (4) the navigation is a primary main frame navigation or it's for |
| // prerendering a main frame, as the cookie change information will only be |
| // used to determined if a page can be restored from back/forward cache, so |
| // subframe navigation can be ignored. |
| return frame_tree_node_->navigator() |
| .controller() |
| .GetBackForwardCache() |
| .should_allow_storing_pages_with_cache_control_no_store() && |
| !IsPageActivation() && !IsSameDocument() && |
| (IsInPrimaryMainFrame() || IsInPrerenderedMainFrame()) && |
| common_params_->url.SchemeIsHTTPOrHTTPS(); |
| } |
| |
| bool NavigationRequest::ShouldAddDeviceBoundSessionObserver() { |
| // Device bound session expiry should evict pages from the BFCache in |
| // the exact same circumstances as cookie expiry. |
| return ShouldAddCookieChangeListener(); |
| } |
| |
| void NavigationRequest::StartNavigation() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::StartNavigation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| DCHECK(frame_tree_node_->navigation_request() == this || |
| is_synchronous_renderer_commit_); |
| FrameTreeNode* frame_tree_node = frame_tree_node_; |
| |
| MaybeAssignInvalidPrerenderFrameTreeNodeId(); |
| |
| // This is needed to get site URLs and assign the expected RenderProcessHost. |
| // This is not always the same as |source_site_instance_|, as it only depends |
| // on the current frame host, and does not depend on |entry|. |
| // The |starting_site_instance_| needs to be set here instead of the |
| // constructor since a navigation can be started after the constructor and |
| // before here, which can set a different RenderFrameHost and a different |
| // starting SiteInstance. |
| starting_site_instance_ = |
| frame_tree_node->current_frame_host()->GetSiteInstance(); |
| site_info_ = GetSiteInfoForCommonParamsURL(); |
| |
| // It's important to start listening to the cookie changes before the network |
| // request of the navigation begins in order to ensure the listener won't miss |
| // any cookie changes that happen after the network request is sent that |
| // potentially modify some cookie values that are used in this request. |
| // The information of cookie modification will be used to determine if the |
| // document that this navigation will load should be eligible for BFCache. |
| // The listener eventually will be transferred over to the committed |
| // `RenderFrameHost`. |
| if (ShouldAddCookieChangeListener()) { |
| // The listener should receive the change events of the cookies from the |
| // the domain of the main-frame navigation url. |
| // If the navigation gets redirected, it will be reset with the new URL when |
| // `NavigationRequest::OnRequestRedirected()` is called. |
| cookie_change_listener_ = |
| std::make_unique<RenderFrameHostImpl::CookieChangeListener>( |
| GetStoragePartitionWithCurrentSiteInfo(), common_params_->url); |
| } |
| |
| if (ShouldAddDeviceBoundSessionObserver()) { |
| device_bound_session_observer_ = |
| std::make_unique<RenderFrameHostImpl::DeviceBoundSessionObserver>( |
| GetStoragePartitionWithCurrentSiteInfo(), common_params_->url); |
| } |
| |
| // Compute the redirect chain. |
| // TODO(clamy): Try to simplify this and have the redirects be part of |
| // CommonNavigationParams. |
| redirect_chain_.clear(); |
| if (!begin_params_->client_side_redirect_url.is_empty()) { |
| // |begin_params_->client_side_redirect_url| will be set when the navigation |
| // was triggered by a client-side redirect. |
| redirect_chain_.push_back(begin_params_->client_side_redirect_url); |
| } else if (!commit_params_->redirects.empty()) { |
| // Redirects that were specified at NavigationRequest creation time should |
| // be added to the list of redirects. In particular, if the |
| // NavigationRequest was created at commit time, redirects that happened |
| // during the navigation have been added to |commit_params_->redirects| and |
| // should be passed to the NavigationHandle. |
| for (const auto& url : commit_params_->redirects) |
| redirect_chain_.push_back(url); |
| } |
| |
| // Finally, add the current URL to the vector of redirects. |
| // Note: for NavigationRequests created at commit time, the current URL has |
| // been added to |commit_params_->redirects|, so don't add it a second time. |
| if (!is_synchronous_renderer_commit_) { |
| if (!common_params_->base_url_for_data_url.is_empty()) { |
| // If this is a loadDataWithBaseURL/loadDataAsStringWithBaseUrl |
| // navigation, use the base URL instead of the data: URL used for commit. |
| redirect_chain_.push_back(common_params_->base_url_for_data_url); |
| } else { |
| redirect_chain_.push_back(common_params_->url); |
| } |
| } |
| |
| // Mirrors the logic in RenderFrameImpl::SendDidCommitProvisionalLoad. |
| if (common_params_->transition & ui::PAGE_TRANSITION_CLIENT_REDIRECT) { |
| // If the page contained a client redirect (meta refresh, |
| // document.location), set the referrer appropriately. |
| // Note that this value will stay the same, even after cross-origin |
| // redirects. This means the referrer URL and policy in |
| // `sanitized_referrer_` might not be the same as the actual referrer sent |
| // for the final navigation request (which will be updated/re-sanitized on |
| // each redirect). |
| // TODO(crbug.com/40771822): Remove this special case, and also |
| // `sanitized_referrer_` entirely, in favor of CommonNavigationParams' |
| // `referrer`, which will be properly sanitized after each redirect. |
| sanitized_referrer_ = blink::mojom::Referrer::New( |
| redirect_chain_[0], Referrer::SanitizeForRequest( |
| common_params_->url, *common_params_->referrer) |
| ->policy); |
| } else { |
| sanitized_referrer_ = Referrer::SanitizeForRequest( |
| common_params_->url, *common_params_->referrer); |
| } |
| |
| // If the navigation explicitly requested for history list clearing (e.g. when |
| // running layout tests), don't do a replacement (since there won't be any |
| // entry to replace after the navigation). |
| if (commit_params_->should_clear_history_list) { |
| common_params_->should_replace_current_entry = false; |
| } else if ( |
| ShouldReplaceCurrentEntryForSameUrlNavigation() || |
| ShouldReplaceCurrentEntryForNavigationFromInitialEmptyDocumentOrEntry()) { |
| common_params_->should_replace_current_entry = true; |
| } |
| |
| // Set the expected process for this navigation, if we can. The navigation |
| // might not have an associated RenderFrameHost yet, which is possible if it |
| // can't create a speculative RenderFrameHost when there's a pending commit |
| // navigation (when navigation queueing is enabled), or it had an associated |
| // RenderFrameHost when the NavigationRequest was created but another |
| // navigation had committed in between that time and StartNavigation, which |
| // invalidates the `associated_rfh_type_`, or it's intentionally deferred with |
| // feature flag DeferSpeculativeRFHCreation. It's fine to skip setting the |
| // expected process in this case, as we'll set the expected process again from |
| // ReadyToCommitNavigation(), when we know the final RenderFrameHost for the |
| // navigation. |
| SetExpectedProcessIfAssociated(); |
| |
| DCHECK(!IsNavigationStarted()); |
| SetState(WILL_START_REQUEST); |
| is_navigation_started_ = true; |
| |
| modified_request_headers_.Clear(); |
| removed_request_headers_.clear(); |
| |
| throttle_registry_ = std::make_unique<NavigationThrottleRegistryImpl>(this); |
| |
| // For prerendered page activation, CommitDeferringConditions have already run |
| // at the beginning of the navigation, so we won't run them again. |
| if (!IsPrerenderedPageActivation()) { |
| commit_deferrer_ = CommitDeferringConditionRunner::Create( |
| *this, CommitDeferringCondition::NavigationType::kOther, |
| /*candidate_prerender_frame_tree_node_id=*/std::nullopt); |
| } |
| |
| navigation_visible_to_embedder_ = true; |
| #if BUILDFLAG(IS_ANDROID) |
| // Once the navigation has started, fill in the details in the Java side |
| // navigation handle. |
| navigation_handle_proxy_->DidStart(); |
| #endif |
| |
| if (IsInMainFrame()) { |
| DCHECK(!common_params_->navigation_start.is_null()); |
| DCHECK(!blink::IsRendererDebugURL(common_params_->url)); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1( |
| "navigation", "Navigation StartToCommit", |
| TRACE_ID_WITH_SCOPE("StartToCommit", TRACE_ID_LOCAL(this)), |
| common_params_->navigation_start, "Initial URL", |
| common_params_->url.spec()); |
| } |
| |
| if (IsSameDocument()) { |
| EnterChildTraceEvent("Same document", this); |
| } |
| |
| { |
| #if DCHECK_IS_ON() |
| DCHECK(is_safe_to_delete_); |
| base::AutoReset<bool> resetter(&is_safe_to_delete_, false); |
| #endif |
| base::AutoReset<bool> resetter2(&ua_change_requires_reload_, false); |
| GetDelegate()->DidStartNavigation(this); |
| } |
| } |
| |
| void NavigationRequest::ResetForCrossDocumentRestart() { |
| DCHECK(IsSameDocument()); |
| |
| // TODO(crbug.com/40055210): A same document history navigation was performed |
| // but the renderer thinks there's a different document loaded. Where did |
| // this navigation come from? |
| if (common_params_->navigation_type == |
| blink::mojom::NavigationType::HISTORY_SAME_DOCUMENT) { |
| CaptureTraceForNavigationDebugScenario( |
| DebugScenario::kDebugSameDocNavigationDocIdMismatch); |
| } |
| |
| // Reset the NavigationHandle, which is now incorrectly marked as |
| // same-document. Ensure |loader_| does not exist as it can hold raw pointers |
| // to objects owned by the handle (see the comment in the header). |
| DCHECK(!loader_); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (navigation_visible_to_embedder_) |
| navigation_handle_proxy_->DidFinish(); |
| #endif |
| |
| // Set this bit so the observers on `DidFinishNavigation()` are also aware of |
| // the restart. |
| was_reset_for_cross_document_restart_ = true; |
| |
| // It is necessary to call DidFinishNavigation before resetting |
| // |navigation_handle_proxy_|. See https://crbug.com/958396. |
| if (IsNavigationStarted()) { |
| GetDelegate()->DidFinishNavigation(this); |
| if (IsInMainFrame()) { |
| TRACE_EVENT_NESTABLE_ASYNC_END2( |
| "navigation", "Navigation StartToCommit", |
| TRACE_ID_WITH_SCOPE("StartToCommit", TRACE_ID_LOCAL(this)), "URL", |
| common_params_->url.spec(), "Net Error Code", net_error_); |
| } |
| } |
| |
| // Reset the state of the NavigationRequest, and the navigation_handle_id. |
| StopCommitTimeout(); |
| SetState(NOT_STARTED); |
| is_navigation_started_ = false; |
| processing_navigation_throttle_ = false; |
| |
| navigation_visible_to_embedder_ = false; |
| #if BUILDFLAG(IS_ANDROID) |
| if (navigation_visible_to_embedder_) { |
| navigation_handle_proxy_.reset(); |
| navigation_handle_proxy_ = std::make_unique<NavigationHandleProxy>(this); |
| } |
| #endif |
| |
| // Reset the previously selected RenderFrameHost. This is expected to be null |
| // at the beginning of a new navigation. See https://crbug.com/936962. |
| DCHECK(HasRenderFrameHost()); |
| render_frame_host_ = std::nullopt; |
| |
| // Convert the navigation type to the appropriate cross-document one. |
| common_params_->navigation_type = |
| ConvertToCrossDocumentType(common_params_->navigation_type); |
| |
| // Reset navigation handle timings. |
| navigation_handle_timing_ = NavigationHandleTiming(); |
| |
| policy_container_builder_->ResetForCrossDocumentRestart(); |
| commit_params_->soft_navigation_heuristics_task_id = std::nullopt; |
| |
| CheckSoftNavigationHeuristicsInvariants(); |
| } |
| |
| void NavigationRequest::ResetStateForSiteInstanceChange() { |
| // This method should only be called when there is a dest_site_instance. |
| DCHECK(dest_site_instance_); |
| |
| // When a request has a destination SiteInstance (e.g., reload or session |
| // history navigation) but it changes during the navigation (e.g., due to |
| // redirect or error page), it's important not to remember privileges or |
| // attacker-controlled state from the original entry. |
| |
| // Reset bindings (e.g., since error pages for WebUI URLs don't get them). |
| bindings_.reset(); |
| |
| // Reset any existing PageState with a non-empty, clean PageState, so that old |
| // attacker-controlled state is not pulled into the new process. |
| blink::PageState page_state = |
| blink::PageState::CreateFromEncodedData(commit_params_->page_state); |
| if (page_state.IsValid()) |
| commit_params_->page_state = |
| blink::PageState::CreateFromURL(GetURL()).ToEncodedData(); |
| |
| // ISNs and DSNs are process-specific. |
| frame_entry_item_sequence_number_ = -1; |
| frame_entry_document_sequence_number_ = -1; |
| } |
| |
| void NavigationRequest::RegisterSubresourceOverride( |
| blink::mojom::TransferrableURLLoaderPtr transferrable_loader) { |
| if (!transferrable_loader) |
| return; |
| if (!subresource_overrides_) |
| subresource_overrides_.emplace(); |
| |
| subresource_overrides_->push_back(std::move(transferrable_loader)); |
| } |
| |
| mojom::NavigationClient* NavigationRequest::GetCommitNavigationClient() { |
| if (commit_navigation_client_ && commit_navigation_client_.is_bound()) |
| return commit_navigation_client_.get(); |
| |
| // Instantiate a new NavigationClient interface. |
| commit_navigation_client_ = |
| GetRenderFrameHost()->GetNavigationClientFromInterfaceProvider(); |
| HandleInterfaceDisconnection(commit_navigation_client_); |
| return commit_navigation_client_.get(); |
| } |
| |
| void NavigationRequest::SetRequiredCSP( |
| network::mojom::ContentSecurityPolicyPtr csp) { |
| DCHECK(!required_csp_); |
| required_csp_ = std::move(csp); |
| if (required_csp_) |
| SetRequestHeader("Sec-Required-CSP", required_csp_->header->header_value); |
| } |
| |
| network::mojom::ContentSecurityPolicyPtr NavigationRequest::TakeRequiredCSP() { |
| return std::move(required_csp_); |
| } |
| |
| const PolicyContainerPolicies* |
| NavigationRequest::GetInitiatorPolicyContainerPolicies() const { |
| return policy_container_builder_->InitiatorPolicies(); |
| } |
| |
| const blink::DocumentToken& NavigationRequest::GetDocumentToken() const { |
| DCHECK(!IsSameDocument()); |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| |
| return *document_token_; |
| } |
| |
| const PolicyContainerPolicies& NavigationRequest::GetPolicyContainerPolicies() |
| const { |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| |
| return policy_container_builder_->FinalPolicies(); |
| } |
| |
| blink::mojom::PolicyContainerPtr |
| NavigationRequest::CreatePolicyContainerForBlink() { |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| |
| return policy_container_builder_->CreatePolicyContainerForBlink(); |
| } |
| scoped_refptr<PolicyContainerHost> NavigationRequest::GetPolicyContainerHost() { |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| // It is invalid calling this method after `TakePolicyContainerHost()`. |
| CHECK(policy_container_builder_); |
| return policy_container_builder_->GetPolicyContainerHost(); |
| } |
| |
| scoped_refptr<PolicyContainerHost> |
| NavigationRequest::TakePolicyContainerHost() { |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| |
| // Move the host out of the data member, then reset the member. This ensures |
| // we do not use the helper after we moved its contents. |
| scoped_refptr<PolicyContainerHost> host = |
| std::move(*policy_container_builder_).TakePolicyContainerHost(); |
| policy_container_builder_ = std::nullopt; |
| |
| return host; |
| } |
| |
| void NavigationRequest::CreateCoepReporter( |
| StoragePartition* storage_partition) { |
| DCHECK(!isolation_info_for_subresources_.IsEmpty()); |
| |
| const PolicyContainerPolicies& policies = |
| policy_container_builder_->FinalPolicies(); |
| coep_reporter_ = std::make_unique<CrossOriginEmbedderPolicyReporter>( |
| static_cast<StoragePartitionImpl*>(storage_partition)->GetWeakPtr(), |
| common_params_->url, |
| policies.cross_origin_embedder_policy.reporting_endpoint, |
| policies.cross_origin_embedder_policy.report_only_reporting_endpoint, |
| GetRenderFrameHost()->GetFrameToken().value(), |
| isolation_info_for_subresources_.network_anonymization_key()); |
| } |
| |
| std::unique_ptr<CrossOriginEmbedderPolicyReporter> |
| NavigationRequest::TakeCoepReporter() { |
| return std::move(coep_reporter_); |
| } |
| |
| void NavigationRequest::CreateDipReporter(StoragePartition* storage_partition) { |
| DCHECK(!isolation_info_for_subresources_.IsEmpty()); |
| |
| const PolicyContainerPolicies& policies = |
| policy_container_builder_->FinalPolicies(); |
| dip_reporter_ = std::make_unique<DocumentIsolationPolicyReporter>( |
| static_cast<StoragePartitionImpl*>(storage_partition)->GetWeakPtr(), |
| common_params_->url, |
| policies.document_isolation_policy.reporting_endpoint, |
| policies.document_isolation_policy.report_only_reporting_endpoint, |
| GetRenderFrameHost()->GetFrameToken().value(), |
| isolation_info_for_subresources_.network_anonymization_key()); |
| } |
| |
| std::unique_ptr<DocumentIsolationPolicyReporter> |
| NavigationRequest::TakeDipReporter() { |
| return std::move(dip_reporter_); |
| } |
| |
| ukm::SourceId NavigationRequest::GetPreviousPageUkmSourceId() { |
| return previous_page_ukm_source_id_; |
| } |
| |
| void NavigationRequest::OnRequestRedirected( |
| const net::RedirectInfo& redirect_info, |
| const net::NetworkAnonymizationKey& network_anonymization_key, |
| network::mojom::URLResponseHeadPtr response_head) { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::OnRequestRedirected", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| ScopedCrashKeys crash_keys(*this); |
| |
| // Sanity check - this can only be set at commit time. |
| DCHECK(!auth_challenge_info_); |
| |
| DCHECK(response_head); |
| DCHECK(response_head->parsed_headers); |
| response_head_ = std::move(response_head); |
| ssl_info_ = response_head_->ssl_info; |
| |
| // Reset the page state as it can no longer be used at commit time since the |
| // navigation was redirected. |
| commit_params_->page_state = std::string(); |
| |
| // Reset NotRestoredReasons as the reasons are for the original page and not |
| // for the redirected one. |
| commit_params_->not_restored_reasons = nullptr; |
| |
| // Reset the tentative origin_to_commit, as the redirected one is different. |
| tentative_data_origin_to_commit_ = std::nullopt; |
| |
| // A request was made. Record it before we decide to block this response for |
| // a reason or another. |
| RecordAddressSpaceFeature(); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| base::WeakPtr<NavigationRequest> this_ptr(weak_factory_.GetWeakPtr()); |
| |
| bool should_override_url_loading = false; |
| if (!GetContentClient()->browser()->ShouldOverrideUrlLoading( |
| frame_tree_node_->frame_tree_node_id(), |
| commit_params_->is_browser_initiated, redirect_info.new_url, |
| redirect_info.new_method, |
| // Redirects are always not counted as from user gesture. |
| false, true, frame_tree_node_->IsOutermostMainFrame(), |
| frame_tree_node_->frame_tree().is_prerendering(), |
| ui::PageTransitionFromInt(common_params_->transition), |
| &should_override_url_loading)) { |
| // A Java exception was thrown by the embedding application; we |
| // need to return from this task. Specifically, it's not safe from |
| // this point on to make any JNI calls. |
| return; |
| } |
| |
| // The content/ embedder might cause |this| to be deleted while |
| // |ShouldOverrideUrlLoading| is called. |
| // See https://crbug.com/770157. |
| if (!this_ptr) |
| return; |
| |
| if (should_override_url_loading) { |
| net_error_ = net::ERR_ABORTED; |
| extended_error_code_ = |
| static_cast<int32_t>(ErrorNavigationTrigger::kShouldOverrideUrlLoading); |
| common_params_->url = redirect_info.new_url; |
| common_params_->method = redirect_info.new_method; |
| // Update the navigation handle to point to the new url to ensure |
| // AwWebContents sees the new URL and thus passes that URL to onPageFinished |
| // (rather than passing the old URL). |
| UpdateStateFollowingRedirect(GURL(redirect_info.new_referrer)); |
| frame_tree_node_->ResetNavigationRequest( |
| NavigationDiscardReason::kInternalCancellation); |
| return; |
| } |
| #endif |
| if (!ChildProcessSecurityPolicyImpl::GetInstance()->CanRedirectToURL( |
| redirect_info.new_url)) { |
| DVLOG(1) << "Denied redirect for " |
| << redirect_info.new_url.possibly_invalid_spec(); |
| // TODO(arthursonzogni): Redirect to a javascript URL should display an |
| // error page with the net::ERR_UNSAFE_REDIRECT error code. Instead, the |
| // browser simply ignores the navigation, because some extensions use this |
| // edge case to silently cancel navigations. See https://crbug.com/941653. |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = |
| static_cast<int32_t>(ErrorNavigationTrigger::kRedirectNotAllowed); |
| OnRequestFailedInternal(completion_status, false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // For renderer-initiated navigations we need to check if the source has |
| // access to the URL. Browser-initiated navigations only rely on the |
| // |CanRedirectToURL| test above. |
| // TODO(crbug.com/388998723): The check may unintentionally create a process |
| // for the source site instance. Consider removing this potential process |
| // creation while keeping the security check. |
| if (!commit_params_->is_browser_initiated && GetSourceSiteInstance() && |
| !ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL( |
| GetSourceSiteInstance() |
| ->GetOrCreateProcess(ProcessAllocationContext{ |
| ProcessAllocationSource::kCanRequestURL}) |
| ->GetDeprecatedID(), |
| redirect_info.new_url)) { |
| DVLOG(1) << "Denied unauthorized redirect for " |
| << redirect_info.new_url.possibly_invalid_spec(); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kRendererInitiatedCanNotRequestURL); |
| // TODO(arthursonzogni): This case uses ERR_ABORTED to be consistent with |
| // the javascript URL redirect case above, though ideally it would use |
| // net::ERR_UNSAFE_REDIRECT and an error page. See https://crbug.com/941653. |
| OnRequestFailedInternal(completion_status, false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| const std::optional<network::mojom::BlockedByResponseReason> |
| coop_requires_blocking = |
| coop_status_.SanitizeResponse(response_head_.get()); |
| if (coop_requires_blocking) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(*coop_requires_blocking), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| const url::Origin origin = GetOriginForURLLoaderFactoryUnchecked(); |
| coop_status_.EnforceCOOP( |
| response()->parsed_headers->cross_origin_opener_policy, origin, |
| network_anonymization_key); |
| |
| const std::optional<network::mojom::BlockedByResponseReason> |
| coep_requires_blocking = EnforceCOEP(); |
| if (coep_requires_blocking) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(*coep_requires_blocking), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // For now, DevTools needs the POST data sent to the renderer process even if |
| // it is no longer a POST after the redirect. |
| if (redirect_info.new_method != "POST") |
| common_params_->post_data.reset(); |
| |
| const bool is_first_response = commit_params_->redirects.empty(); |
| UpdateNavigationHandleTimingsOnResponseReceived(/*is_redirect=*/true, |
| is_first_response); |
| |
| // Mark time for the Navigation Timing API. |
| if (commit_params_->navigation_timing->redirect_start.is_null()) { |
| commit_params_->navigation_timing->redirect_start = |
| commit_params_->navigation_timing->fetch_start; |
| } |
| commit_params_->navigation_timing->redirect_end = base::TimeTicks::Now(); |
| commit_params_->navigation_timing->fetch_start = base::TimeTicks::Now(); |
| |
| commit_params_->redirect_response.push_back(response_head_.Clone()); |
| commit_params_->redirect_infos.push_back(redirect_info); |
| |
| // TODO(rakina): This should use `GetTentativeOriginAtRequestTime()` instead |
| // of the url from `common_params_`. |
| const bool is_same_origin_redirect = |
| url::Origin::Create(common_params_->url) |
| .IsSameOriginWith(redirect_info.new_url); |
| |
| did_encounter_cross_origin_redirect_ |= !is_same_origin_redirect; |
| |
| // Only same-origin navigations without cross-origin redirects can |
| // expose response details (status-code / mime-type). |
| // https://github.com/whatwg/fetch/issues/1602 |
| if (!is_same_origin_redirect && |
| commit_params_->navigation_timing->parent_resource_timing_access == |
| blink::mojom::ParentResourceTimingAccess:: |
| kReportWithResponseDetails) { |
| commit_params_->navigation_timing->parent_resource_timing_access = |
| blink::mojom::ParentResourceTimingAccess::kReportWithoutResponseDetails; |
| } |
| |
| did_receive_early_hints_before_cross_origin_redirect_ |= |
| did_create_early_hints_manager_params_ && !is_same_origin_redirect; |
| |
| commit_params_->redirects.push_back(common_params_->url); |
| common_params_->url = redirect_info.new_url; |
| common_params_->method = redirect_info.new_method; |
| common_params_->referrer->url = GURL(redirect_info.new_referrer); |
| common_params_->referrer = Referrer::SanitizeForRequest( |
| common_params_->url, *common_params_->referrer); |
| |
| // On redirects, the initial referrer is no longer correct, so it must |
| // be updated. (A parallel process updates the outgoing referrer in the |
| // network stack.) |
| commit_params_->redirect_infos.back().new_referrer = |
| common_params_->referrer->url.spec(); |
| |
| // When the redirection happens, the cookie_change_listener_ should be |
| // re-initialized if needed. |
| if (ShouldAddCookieChangeListener()) { |
| cookie_change_listener_ = |
| std::make_unique<RenderFrameHostImpl::CookieChangeListener>( |
| GetStoragePartitionWithCurrentSiteInfo(), common_params_->url); |
| } else { |
| cookie_change_listener_.reset(); |
| } |
| |
| if (ShouldAddDeviceBoundSessionObserver()) { |
| device_bound_session_observer_ = |
| std::make_unique<RenderFrameHostImpl::DeviceBoundSessionObserver>( |
| GetStoragePartitionWithCurrentSiteInfo(), common_params_->url); |
| } else { |
| device_bound_session_observer_.reset(); |
| } |
| |
| // Check Content Security Policy before the NavigationThrottles run. This |
| // gives CSP a chance to modify requests that NavigationThrottles would |
| // otherwise block. |
| net::Error net_error = |
| CheckContentSecurityPolicy(true /* has_followed_redirect */, |
| redirect_info.insecure_scheme_was_upgraded, |
| false /* is_response_check */); |
| if (net_error != net::OK) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net_error), false /*skip_throttles*/, |
| std::nullopt /*error_page_content*/, false /*collapse_frame*/); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (CheckCredentialedSubresource() == |
| CredentialedSubresourceCheckResult::BLOCK_REQUEST) { |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kCredentialedSubresourceBlocked); |
| OnRequestFailedInternal(completion_status, false /*skip_throttles*/, |
| std::nullopt /*error_page_content*/, |
| false /*collapse_frame*/); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // Compute the SiteInstance to use for the redirect and pass its |
| // RenderProcessHost if it has a process. Keep a reference if it has a |
| // process, so that the SiteInstance and its associated process aren't deleted |
| // before the navigation is ready to commit. |
| scoped_refptr<SiteInstance> site_instance = |
| frame_tree_node_->render_manager()->GetSiteInstanceForNavigationRequest( |
| this, &browsing_context_group_swap_); |
| speculative_site_instance_ = |
| site_instance->HasProcess() ? site_instance : nullptr; |
| |
| // If the new site instance doesn't yet have a process, then tell the |
| // SpareRenderProcessHostManager so it can decide whether to start warming up |
| // the spare at this time (note that the actual behavior depends on |
| // RenderProcessHostImpl::IsSpareProcessKeptAtAllTimes). |
| if (!site_instance->HasProcess()) { |
| RenderProcessHostImpl::NotifySpareManagerAboutRecentlyUsedSiteInstance( |
| site_instance.get()); |
| } |
| |
| // Check what the process of the SiteInstance is. It will be passed to the |
| // NavigationHandle, and informed to expect a navigation to the redirected |
| // URL. |
| // TODO(crbug.com/388998723): Remove the comment about the side effect |
| // of GetProcess() after the full migration to GetOrCreateProcess(). |
| // Note: calling GetProcess on the SiteInstance can lead to the creation of a |
| // new process if it doesn't have one. In this case, it should only be called |
| // on a SiteInstance that already has a process. |
| RenderProcessHost* expected_process = |
| site_instance->HasProcess() ? site_instance->GetProcess() : nullptr; |
| |
| WillRedirectRequest(common_params_->referrer->url, expected_process); |
| } |
| |
| base::WeakPtr<NavigationRequest> NavigationRequest::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| base::SafeRef<NavigationHandle> NavigationRequest::GetSafeRef() { |
| return weak_factory_.GetSafeRef(); |
| } |
| |
| bool NavigationRequest::ExistingDocumentWasDiscarded() const { |
| return commit_params_->was_discarded; |
| } |
| |
| void NavigationRequest::SetContentSettings( |
| blink::mojom::RendererContentSettingsPtr content_settings) { |
| commit_params_->content_settings = std::move(content_settings); |
| } |
| |
| blink::mojom::RendererContentSettingsPtr |
| NavigationRequest::GetContentSettingsForTesting() { |
| return commit_params_->content_settings->Clone(); |
| } |
| |
| void NavigationRequest::SetIsAdTagged() { |
| is_ad_tagged_ = true; |
| } |
| |
| void NavigationRequest::CheckForIsolationOptIn(const GURL& url) { |
| // Check whether an origin-keyed agent cluster is explicitly requested, either |
| // opting in or out, before attempting to isolate it. If an explicit request |
| // was made, then we must check if the origin has been previously |
| // encountered in order to remain consistent within the isolation context |
| // (BrowserContext). Note: we only do the global walk for explicit opt-outs |
| // when OriginAgentCluster-by-default is enabled, but that check is made in |
| // IsOriginAgentClusterOptOutRequested(). |
| if (!IsOriginAgentClusterOptInRequested() && |
| !IsOriginAgentClusterOptOutRequested()) |
| return; |
| |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| url::Origin origin = url::Origin::Create(url); |
| auto* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| if (policy->UpdateOriginIsolationOptInListIfNecessary(browser_context, |
| origin)) { |
| // This is a new request for isolating |origin|, either by explicitly opting |
| // it in or out. Do a global walk of session history to find any existing |
| // instances of |origin|, so that those existing BrowsingInstances can give |
| // it default isolation. Only new BrowsingInstances and ones that have not |
| // seen |origin| before will honor the request. We don't always have a value |
| // for render_frame_host_ at this point, so we map the global-walk call onto |
| // NavigatorDelegate to get it into WebContents. We definitely need to do |
| // the global walk prior to deciding on the render_frame_host_ to commit to. |
| // We must exclude ourselves from the global walk otherwise we may mark our |
| // origin as having default isolation before it gets the change to register |
| // itself as opted-in/out. |
| frame_tree_node_->navigator() |
| .GetDelegate() |
| ->RegisterExistingOriginAsHavingDefaultIsolation( |
| origin, this /* navigation_request_to_exclude */); |
| } |
| } |
| |
| void NavigationRequest::AddOriginAgentClusterStateIfNecessary( |
| const IsolationContext& isolation_context) { |
| // Normally for explicit opt-ins the origin is tracked when we create the |
| // SiteInstance, but there are two cases where that fails. (1) If process- |
| // isolation for OAC is not enabled we need to track opt-in here (used for |
| // origin-agent-cluster-by-default), and (2) if origin-keyed processes by |
| // default is enabled, then it's possible we got here due to using a |
| // speculative RenderFrameHost. In this latter case, the opt-in header had not |
| // arrived when the SiteInstance was created, so the origin was not tracked |
| // earlier. |
| bool is_opt_in_requested = IsOriginAgentClusterOptInRequested(); |
| bool explicitly_requests_origin_keyed_process = |
| is_opt_in_requested && |
| SiteIsolationPolicy::IsProcessIsolationForOriginAgentClusterEnabled(); |
| |
| // Since opt-outs are asking not to have OAC or requires_origin_keyed_process, |
| // they don't get their own SiteInstance, and so we must register their |
| // opt-out here. |
| bool is_opt_out_requested = IsOriginAgentClusterOptOutRequested(); |
| |
| // We never register isolation state here unless it's explicitly requested. |
| if (!is_opt_in_requested && !is_opt_out_requested) |
| return; |
| |
| bool should_isolate_origin = is_opt_in_requested; |
| |
| // Note: we don't handle IsIsolationImplied() cases here, since those only |
| // occur when OAC-by-default is enabled, and in that case we only pro-actively |
| // record explicit opt-ins and opt-outs. Implicitly isolated origins only end |
| // up recorded if a future request from the same origin attempts to opt-in or |
| // opt-out, which would trigger a normal global walk and record that the |
| // origin has already been implicitly isolated in some BrowsingInstances. |
| |
| // TODO(crbug.com/40910871): investigate using one of NavigationRequest's |
| // Get*Origin*() functions to compute this, instead of assuming we can just |
| // convert directly from GetURL(). |
| url::Origin origin = url::Origin::Create(GetURL()); |
| // Since this origin is using a site-keyed process (either because |
| // origin-keyed processes are disabled, not used for this origin, or the |
| // origin has opted out), we can't rely on a newly created SiteInstance to add |
| // the origin as OAC/not-OAC, so we do it manually here. |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| // If there is already a state registered for `origin` in `isolation_context`, |
| // then the following call does nothing. |
| policy->AddOriginIsolationStateForBrowsingInstance( |
| isolation_context, origin, |
| should_isolate_origin /* is_origin_agent_cluster */, |
| explicitly_requests_origin_keyed_process); |
| } |
| |
| bool NavigationRequest::IsOriginAgentClusterOptInRequested() { |
| // We explicitly do not honor Origin-Agent-Cluster headers in redirects and |
| // may only consider them in final responses, according to spec. |
| // https://crbug.com/1329061 |
| if (state_ < WILL_PROCESS_RESPONSE || state_ == WILL_FAIL_REQUEST) { |
| return false; |
| } |
| |
| if (!response()) { |
| return false; |
| } |
| |
| // Do not attempt isolation if the feature is not enabled. |
| if (!SiteIsolationPolicy::IsOriginAgentClusterEnabled()) { |
| return false; |
| } |
| |
| return response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kTrue; |
| } |
| |
| bool NavigationRequest::IsOriginAgentClusterOptOutRequested() { |
| // We explicitly do not honor Origin-Agent-Cluster headers in redirects and |
| // may only consider them in final responses, according to spec. |
| // https://crbug.com/1329061 |
| if (state_ < WILL_PROCESS_RESPONSE || state_ == WILL_FAIL_REQUEST) { |
| return false; |
| } |
| |
| if (!response()) { |
| return false; |
| } |
| |
| // We only allow explicit opt-outs when OAC-by-default is enabled. The |
| // following check will be false if IsOriginAgentClusterEnabled() is false. |
| if (!SiteIsolationPolicy::AreOriginAgentClustersEnabledByDefault( |
| frame_tree_node_->navigator().controller().GetBrowserContext())) { |
| return false; |
| } |
| |
| return response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kFalse; |
| } |
| |
| bool NavigationRequest::IsIsolationImplied() { |
| if (!SiteIsolationPolicy::AreOriginAgentClustersEnabledByDefault( |
| frame_tree_node_->navigator().controller().GetBrowserContext())) { |
| return false; |
| } |
| |
| return !response() || response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kAbsent; |
| } |
| |
| void NavigationRequest::DetermineOriginAgentClusterEndResult() { |
| DCHECK(state_ == WILL_PROCESS_RESPONSE || |
| state_ == WILL_COMMIT_WITHOUT_URL_LOADER || |
| state_ == WILL_FAIL_REQUEST || state_ == CANCELING); |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| url::Origin origin = GetOriginToCommit().value(); |
| const IsolationContext& isolation_context = |
| GetRenderFrameHost()->GetSiteInstance()->GetIsolationContext(); |
| |
| bool is_requested = IsOriginAgentClusterOptInRequested(); |
| bool expects_origin_agent_cluster = is_requested || IsIsolationImplied(); |
| bool is_origin_keyed_process_implied = |
| IsIsolationImplied() && |
| SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(); |
| bool requires_origin_keyed_process = |
| (is_requested || is_origin_keyed_process_implied) && |
| SiteIsolationPolicy::IsProcessIsolationForOriginAgentClusterEnabled(); |
| |
| OriginAgentClusterIsolationState requested_isolation_state = |
| expects_origin_agent_cluster |
| ? OriginAgentClusterIsolationState::CreateForOriginAgentCluster( |
| requires_origin_keyed_process) |
| : OriginAgentClusterIsolationState::CreateNonIsolated(); |
| |
| const bool got_origin_agent_cluster = |
| policy |
| ->DetermineOriginAgentClusterIsolation(isolation_context, origin, |
| requested_isolation_state) |
| .is_origin_agent_cluster(); |
| |
| if (SiteIsolationPolicy::AreOriginAgentClustersEnabledByDefault( |
| frame_tree_node_->navigator().controller().GetBrowserContext())) { |
| // When OAC is enabled by default, report enum values that distinguish |
| // between explicitly requesting OAC (on or off) and having no related |
| // header. |
| bool was_explicitly_requested = |
| response_head_ && |
| response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kTrue; |
| bool was_explicitly_not_requested = |
| response_head_ && |
| response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kFalse; |
| |
| if (got_origin_agent_cluster) { |
| if (was_explicitly_requested) { |
| origin_agent_cluster_end_result_ = |
| OriginAgentClusterEndResult::kExplicitlyRequestedAndOriginKeyed; |
| } else if (was_explicitly_not_requested) { |
| origin_agent_cluster_end_result_ = |
| OriginAgentClusterEndResult::kExplicitlyNotRequestedButOriginKeyed; |
| } else { |
| origin_agent_cluster_end_result_ = |
| OriginAgentClusterEndResult::kNotExplicitlyRequestedAndOriginKeyed; |
| } |
| } else { |
| if (was_explicitly_requested) { |
| origin_agent_cluster_end_result_ = |
| OriginAgentClusterEndResult::kExplicitlyRequestedButNotOriginKeyed; |
| } else if (was_explicitly_not_requested) { |
| origin_agent_cluster_end_result_ = OriginAgentClusterEndResult:: |
| kExplicitlyNotRequestedAndNotOriginKeyed; |
| } else { |
| origin_agent_cluster_end_result_ = OriginAgentClusterEndResult:: |
| kNotExplicitlyRequestedButNotOriginKeyed; |
| } |
| } |
| } else { |
| // When OAC is not enabled by default, report enum values that only indicate |
| // if OAC was requested or not vs whether it took effect. |
| if (is_requested) { |
| origin_agent_cluster_end_result_ = |
| got_origin_agent_cluster |
| ? OriginAgentClusterEndResult::kRequestedAndOriginKeyed |
| : OriginAgentClusterEndResult::kRequestedButNotOriginKeyed; |
| } else { |
| origin_agent_cluster_end_result_ = |
| got_origin_agent_cluster |
| ? OriginAgentClusterEndResult::kNotRequestedButOriginKeyed |
| : OriginAgentClusterEndResult::kNotRequestedAndNotOriginKeyed; |
| } |
| } |
| |
| // This needs to be computed separately from origin.opaque() because, per |
| // https://crbug.com/1041376, we don't have a notion of the true origin yet. |
| const bool is_opaque_origin_because_sandbox = |
| (policy_container_builder_->FinalPolicies().sandbox_flags & |
| network::mojom::WebSandboxFlags::kOrigin) == |
| network::mojom::WebSandboxFlags::kOrigin; |
| |
| // The origin_agent_cluster navigation commit parameter communicates to the |
| // renderer about origin-keying, so it should be true for opaque origin |
| // cases (e.g., for data: URLs). origin_agent_cluster_end_result_ shouldn't be |
| // modified since it's used for warnings and use counters, i.e. things that |
| // don't apply to this sort of "automatic" origin-keying. |
| commit_params_->origin_agent_cluster = is_opaque_origin_because_sandbox || |
| origin.opaque() || |
| got_origin_agent_cluster; |
| |
| // The origin_agent_cluster_left_as_default navigation commit parameter |
| // communicates to the renderer whether the origin_agent_cluster decision |
| // (recorded just above) has been made based on an absent Origin-Agent-Cluster |
| // http header. |
| commit_params_->origin_agent_cluster_left_as_default = |
| !response_head_ || response_head_->parsed_headers->origin_agent_cluster == |
| network::mojom::OriginAgentClusterValue::kAbsent; |
| } |
| |
| void NavigationRequest::ProcessOriginAgentClusterEndResult() { |
| if (!HasCommitted() || IsErrorPage() || IsSameDocument()) |
| return; |
| |
| if (origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kRequestedAndOriginKeyed || |
| origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kRequestedButNotOriginKeyed || |
| origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kExplicitlyRequestedAndOriginKeyed || |
| origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kExplicitlyRequestedButNotOriginKeyed) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| GetRenderFrameHost(), |
| blink::mojom::WebFeature::kOriginAgentClusterHeader); |
| } |
| |
| const url::Origin origin = url::Origin::Create(GetURL()); |
| |
| if (origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kRequestedButNotOriginKeyed || |
| origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kExplicitlyRequestedButNotOriginKeyed) { |
| GetRenderFrameHost()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| base::StringPrintf( |
| "The page requested an origin-keyed agent cluster using the " |
| "Origin-Agent-Cluster header, but could not be origin-keyed since " |
| "the origin '%s' had previously been placed in a site-keyed agent " |
| "cluster. Update your headers to uniformly request origin-keying " |
| "for all pages on the origin.", |
| origin.Serialize().c_str())); |
| } |
| |
| if (origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kNotRequestedButOriginKeyed || |
| origin_agent_cluster_end_result_ == |
| OriginAgentClusterEndResult::kExplicitlyNotRequestedButOriginKeyed) { |
| GetRenderFrameHost()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| base::StringPrintf( |
| "The page did not request an origin-keyed agent cluster, but was " |
| "put in one anyway because the origin '%s' had previously been " |
| "placed in an origin-keyed agent cluster. Update your headers to " |
| "uniformly request origin-keying for all pages on the origin.", |
| origin.Serialize().c_str())); |
| } |
| } |
| |
| void NavigationRequest::PopulateDocumentTokenForCrossDocumentNavigation() { |
| DCHECK(!IsSameDocument()); |
| DCHECK_GE(state_, READY_TO_COMMIT); |
| const auto* token_to_reuse = |
| GetRenderFrameHost()->GetDocumentTokenForCrossDocumentNavigationReuse( |
| /* passkey */ {}); |
| document_token_.emplace(token_to_reuse ? *token_to_reuse |
| : blink::DocumentToken()); |
| } |
| |
| bool NavigationRequest::HasCommittingOrigin(const url::Origin& origin) { |
| // We are only interested in checking requests that have been assigned a |
| // SiteInstance. |
| if (state() < WILL_PROCESS_RESPONSE) |
| return false; |
| |
| // This origin conversion won't be correct for about:blank, but origin |
| // isolation shouldn't need to care about that case because a previous |
| // instance of the origin would already have determined its isolation status |
| // in that BrowsingInstance. |
| // TODO(crbug.com/40092527): Use the computed origin here just to be |
| // safe. |
| return origin == url::Origin::Create(GetURL()); |
| } |
| |
| bool NavigationRequest::ShouldRequestSiteIsolationForCOOP() { |
| if (!SiteIsolationPolicy::IsSiteIsolationForCOOPEnabled()) |
| return false; |
| |
| // COOP headers are only served once a response is available. |
| if (state_ < WILL_PROCESS_RESPONSE) |
| return false; |
| |
| // COOP isolation can only be triggered from main frames. COOP headers |
| // aren't honored in subframes. |
| if (!IsInMainFrame()) |
| return false; |
| |
| // Filter out URLs with origins that are considered invalid for being |
| // isolated. Note that the origin we'll eventually attempt to isolate should |
| // be based on process_lock_url(), so that we apply isolation to the actual |
| // site rather than the effective URL in the case of hosted apps. |
| url::Origin origin(url::Origin::Create(site_info_.process_lock_url())); |
| if (!IsolatedOriginUtil::IsValidIsolatedOrigin(origin)) |
| return false; |
| |
| // Check the COOP header value. All same-origin values are considered to be |
| // an implicit hint for site isolation. |
| bool should_header_value_trigger_isolation = false; |
| switch (coop_status_.current_coop().value) { |
| case network::mojom::CrossOriginOpenerPolicyValue::kSameOrigin: |
| case network::mojom::CrossOriginOpenerPolicyValue::kSameOriginPlusCoep: |
| case network::mojom::CrossOriginOpenerPolicyValue::kSameOriginAllowPopups: |
| case network::mojom::CrossOriginOpenerPolicyValue::kNoopenerAllowPopups: |
| should_header_value_trigger_isolation = true; |
| break; |
| case network::mojom::CrossOriginOpenerPolicyValue::kUnsafeNone: |
| should_header_value_trigger_isolation = false; |
| break; |
| // Don't handle the default case on purpose to force a compiler error if |
| // new COOP values are added, so that they are explicitly handled here. |
| } |
| if (!should_header_value_trigger_isolation) |
| return false; |
| |
| // There's no need for additional isolation if the site already requires a |
| // dedicated process via other isolation mechanisms. However, we still |
| // return true if the site has been isolated due to COOP previously, so that |
| // we can go through the COOP isolation flow to update the timestamp of when |
| // the COOP isolation for this site was last used. |
| // |
| // Note: we can use `site_info_` here, since that has been assigned at |
| // request start time and updated by redirects, but it is not (currently) |
| // recomputed when response is received, so it does not include the COOP |
| // isolation request (which would cause RequiresDedicatedProcess to return |
| // true regardless of prior isolation). If we ever decide to update |
| // `site_info_` at response time, we should revisit this and ensure that we |
| // call RequiresDedicatedProcess on a SiteInfo that does not already have an |
| // isolation request (enforced by DCHECK below). |
| DCHECK(!site_info_.does_site_request_dedicated_process_for_coop()); |
| if (site_info_.RequiresDedicatedProcess( |
| GetStartingSiteInstance()->GetIsolationContext())) { |
| bool is_already_isolated_due_to_coop = |
| ChildProcessSecurityPolicyImpl::GetInstance()->IsIsolatedSiteFromSource( |
| origin, |
| ChildProcessSecurityPolicy::IsolatedOriginSource::WEB_TRIGGERED); |
| return is_already_isolated_due_to_coop; |
| } |
| return true; |
| } |
| |
| UrlInfo NavigationRequest::GetUrlInfo() { |
| // Compute the isolation request flags. Note that multiple requests could be |
| // active simultaneously for the same navigation. |
| // We start by assuming that the default isolation will be used, and only |
| // change it if an explicit opt-in or opt-out request is seen. Depending on |
| // the value of OriginAgentClusterIsolationState::CreateForDefaultIsolation, |
| // default isolation could potentially be non-isolated, origin-agent-cluster, |
| // or origin-agent-cluster in an origin-keyed process. Note: the |
| // IsOriginIsolationImplied() case is handled via kDefault. It is the only |
| // case where the `Origin-Agent-Cluster` header is absent. |
| uint32_t isolation_flags = UrlInfo::OriginIsolationRequest::kDefault; |
| |
| if (IsOriginAgentClusterOptOutRequested()) { |
| isolation_flags = UrlInfo::OriginIsolationRequest::kNone; |
| } else if (IsOriginAgentClusterOptInRequested()) { |
| // An origin-keyed agent cluster is used if explicitly requested by header. |
| isolation_flags = |
| UrlInfo::OriginIsolationRequest::kOriginAgentClusterByHeader; |
| if (SiteIsolationPolicy::IsProcessIsolationForOriginAgentClusterEnabled()) { |
| // An origin-keyed process is used if requested by header. |
| isolation_flags |= |
| UrlInfo::OriginIsolationRequest::kRequiresOriginKeyedProcessByHeader; |
| } |
| } |
| |
| // Compute the CrossOriginIsolationKey for the navigation. |
| std::optional<AgentClusterKey::CrossOriginIsolationKey> |
| cross_origin_isolation_key = ComputeCrossOriginIsolationKey(); |
| |
| auto isolation_request = |
| static_cast<UrlInfo::OriginIsolationRequest>(isolation_flags); |
| |
| // Compute the WebExposedIsolationInfo that will be bundled into UrlInfo. |
| auto web_exposed_isolation_info = ComputeWebExposedIsolationInfo(); |
| |
| UrlInfoInit url_info_init(GetURL()); |
| url_info_init.WithOriginIsolationRequest(isolation_request) |
| .WithCOOPSiteIsolation(ShouldRequestSiteIsolationForCOOP()) |
| .WithWebExposedIsolationInfo(web_exposed_isolation_info) |
| .WithCrossOriginIsolationKey(cross_origin_isolation_key) |
| .WithIsPdf(is_pdf_); |
| |
| // Navigations with SiteInstances which have fixed storage partition (e.g. |
| // <webview> tags) should always stay in the current StoragePartition. |
| SiteInstanceImpl* current_instance = |
| frame_tree_node_->current_frame_host()->GetSiteInstance(); |
| if (current_instance->IsFixedStoragePartition()) { |
| url_info_init.WithStoragePartitionConfig( |
| current_instance->GetStoragePartitionConfig()); |
| } |
| |
| // Child frames (including fenced frames) should always use the |
| // same StoragePartition as their parent. |
| RenderFrameHostImpl* parent = GetParentFrameOrOuterDocument(); |
| if (parent) { |
| url_info_init.WithStoragePartitionConfig( |
| parent->GetSiteInstance()->GetStoragePartitionConfig()); |
| } |
| |
| if (IsLoadDataWithBaseURL()) { |
| // LoadDataWithBaseURL() navigations also need to explicitly set the origin |
| // to the origin of the base URL. This ensures that the process for this |
| // navigation will eventually be locked to the right origin (i.e., origin of |
| // the base URL rather than the data: URL). |
| // |
| // Note that while LoadDataWithBaseURL() is supported in <webview> tags on |
| // desktop platforms and on Android Webview, only <webview> tags currently |
| // utilize this special case when running in site-isolated mode. Android |
| // Webview doesn't currently lock processes for LoadDataWithBaseURL() |
| // navigations. |
| url_info_init.WithOrigin( |
| url::Origin::Create(common_params().base_url_for_data_url)); |
| } else if (GetURL().IsAboutBlank() && GetInitiatorOrigin().has_value()) { |
| // about:blank inherits its origin from the initiator, so ensure that this |
| // is reflected in the UrlInfo. In the common case, this isn't needed for |
| // process model decisions, since we already leave about:blank in the |
| // source SiteInstance, which corresponds to the initiator (see |
| // `RenderFrameHostManager::CanUseSourceSiteInstance()`). However, in |
| // certain corner cases, the source SiteInstance can't be used, but we |
| // will still need to assign a proper process for about:blank. In that |
| // case, we should honor the initiator origin, so that about:blank ends up |
| // in a process that's locked to that origin, rather than an unlocked |
| // process with an unassigned SiteInstance. The latter would be violating |
| // site isolation guarantees and would be problematic for Citadel |
| // enforcements in |
| // ChildProcessSecurityPolicyImpl::CanAccessDataForOrigin(). See |
| // https://crbug.com/1426928. |
| // |
| // TODO(alexmos): Consider also specifying UrlInfo::origin for about:srcdoc |
| // navigations. This is not currently needed in the SiteInstance |
| // and process assignment paths for srcdoc frames, but doing this might |
| // simplify some of that code and would be good for consistency, since both |
| // about:blank and about:srcdoc inherit the origin per spec |
| // (https://html.spec.whatwg.org/multipage/document-sequences.html#determining-the-origin). |
| url_info_init.WithOrigin(*GetInitiatorOrigin()); |
| } else { |
| // Overriding the origin for a URL is dangerous and only allowed in very |
| // narrow cases which are handled explicitly above. Please think very |
| // carefully about any new cases that need to do this. |
| DCHECK(!url_info_init.origin().has_value()); |
| } |
| |
| // Propagate the tentative origin to commit value (for data: URLs that will be |
| // rendered) to the UrlInfo, to make sure the nonce remains the same |
| // throughout the navigation. |
| if (GetURL().SchemeIs(url::kDataScheme)) { |
| // The function for computing the request's origin depends on the stage of |
| // the request, but the same opaque nonce value is preserved across both |
| // functions for data: URLs. |
| if (state_ < WILL_PROCESS_RESPONSE) { |
| url_info_init.WithOrigin(GetTentativeOriginAtRequestTime()); |
| } else if (response_should_be_rendered_) { |
| // The origin to commit is nullopt for cases that are not rendered (e.g., |
| // downloads), but the UrlInfo does not need the origin for data: URLs in |
| // such cases. |
| url_info_init.WithOrigin(GetOriginToCommit().value()); |
| } |
| } |
| |
| // Determine if the request is for a sandboxed frame or not, and if so whether |
| // the sandboxed frame should get a dedicated process. Setting |
| // `has_origin_restricted_sandbox_flag` to true indicates it should get |
| // process isolation, but only if the site/origin would have qualified for a |
| // dedicated process even without the sandbox flags. |
| // |
| // If PolicyContainer::ComputePoliciesToCommit() has run |
| // `policy_container_builder_` will be valid, but even if it hasn't, we can |
| // speculatively take `commit_params_->frame_policy.sandbox_flags` if we |
| // haven't received the response yet and don't have the final |
| // `policy_container_builder_`, and if the state of the kOrigin flag changes, |
| // we'll detect the change and recompute the target SiteInstance elsewhere. |
| // |
| // In general, about:blank documents should stay in their initiator's process. |
| // If neither the initiator or about:blank is sandboxed, or if both are, then |
| // the about:blank should stay in its parent's process, if only to avoid |
| // needing an extra process that only shows the empty frame (if the parent and |
| // about:blank are sandboxed, they parent cannot script the about:blank |
| // frame). If the initiator is not sandboxed but the about:blank document is |
| // (e.g., due to iframe attributes), then nothing else will be able to script |
| // the empty about:blank document and it is safe to leave it in the same |
| // process. (It is not possible for the initiator to be sandboxed and the |
| // about:blank to not be sandboxed, because about:blank inherits the |
| // sandboxing of its initiator.) |
| bool is_eligible_for_sandboxing = |
| !GetURL().IsAboutBlank() || |
| (source_site_instance_ && |
| source_site_instance_->GetSiteInfo().is_sandboxed()); |
| if (SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled() && |
| is_eligible_for_sandboxing) { |
| // Determine if the frame has the sandbox flag or not. |
| bool has_origin_restricted_sandbox_flag = false; |
| if (policy_container_builder_->HasComputedPolicies()) { |
| has_origin_restricted_sandbox_flag = |
| (policy_container_builder_->FinalPolicies().sandbox_flags & |
| network::mojom::WebSandboxFlags::kOrigin) == |
| network::mojom::WebSandboxFlags::kOrigin; |
| } else { |
| // Note: We'll end up here if this function is called before |
| // ComputePoliciesToCommit(), such as when computing a speculative |
| // RenderFrameHost's SiteInstance before receiving a response. In that |
| // event we use the sandbox flags in commit_params_ as a current "best |
| // estimate". |
| has_origin_restricted_sandbox_flag = |
| (commit_params_->frame_policy.sandbox_flags & |
| network::mojom::WebSandboxFlags::kOrigin) == |
| network::mojom::WebSandboxFlags::kOrigin; |
| } |
| |
| // It's possible that a sandbox attribute can disappear from a frame that |
| // still contains a sandboxed initiator, meaning we won't have sandbox |
| // flags here, but should still respect the sandbox of the initiator. |
| bool should_inherit_initiators_sandbox = |
| GetURL().IsAboutBlank() && source_site_instance_ && |
| source_site_instance_->GetSiteInfo().is_sandboxed(); |
| |
| // Consider isolating sandboxed frames that won't end up as downloads or |
| // 204s. |
| if ((has_origin_restricted_sandbox_flag || |
| should_inherit_initiators_sandbox) && |
| response_should_be_rendered_) { |
| // If the URL under consideration wouldn't qualify for a dedicated process |
| // without the sandbox flags, then it shouldn't qualify even with the |
| // sandbox flag. This is most likely to occur when site isolation is only |
| // partial, as on Android. |
| // |
| // Ideally the IsolationContext would be the one used for the committed |
| // RenderFrameHost at the end of the navigation, since a different set of |
| // origins may require isolation if a BrowsingInstance swap occurs. This |
| // isn't known at the start of the navigation, though, so we use the |
| // current IsolationContext instead. |
| const IsolationContext& isolation_context = |
| current_instance->GetIsolationContext(); |
| if (SiteInfo::Create(isolation_context, UrlInfo(url_info_init)) |
| .RequiresDedicatedProcess(isolation_context)) { |
| // Embedders can identify, via ContentBrowserClient, cases that should |
| // not use isolated sandboxed frames. |
| ContentBrowserClient* client = GetContentClient()->browser(); |
| BrowserContext* context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| url::SchemeHostPort precursor; |
| if (state_ < WILL_PROCESS_RESPONSE) { |
| precursor = GetTentativeOriginAtRequestTime() |
| .GetTupleOrPrecursorTupleIfOpaque(); |
| } else if (GetOriginToCommit()) { |
| precursor = GetOriginToCommit()->GetTupleOrPrecursorTupleIfOpaque(); |
| } else { |
| NOTREACHED() << "No origin-to-commit for sandboxed url = " |
| << GetURL(); |
| } |
| |
| bool client_allows_cross_process_sandboxed_frames = |
| client->ShouldAllowCrossProcessSandboxedFrameForPrecursor( |
| context, precursor.GetURL(), GetURL()); |
| if (client_allows_cross_process_sandboxed_frames) { |
| url_info_init.WithSandbox(true); |
| // If an isolated sandbox is required, and the "per-document" grouping |
| // mode has been specified with kIsolateSandboxedIframes, then we use |
| // a unique document identifier, provided by `navigation_id_`, to |
| // guarantee that each sandboxed iframe gets its own SiteInstance, |
| // even if two or more such documents share a site/origin. Using |
| // navigation_id_ means that each new NavigationRequest (and thus each |
| // document) will get a different value. |
| if (blink::features::kIsolateSandboxedIframesGroupingParam.Get() == |
| blink::features::IsolateSandboxedIframesGrouping::kPerDocument) { |
| url_info_init.WithUniqueSandboxId(navigation_id_); |
| } |
| } |
| } |
| } |
| } |
| |
| // If a prefetch might have been affected by cross-site state, the |
| // relationship with other windows should be severed to make this more |
| // difficult to use to leak cross-site state. |
| // https://crbug.com/1439246 |
| if (base::FeatureList::IsEnabled( |
| features::kPrefetchStateContaminationMitigation) && |
| response_head_ && |
| response_head_->is_prefetch_with_cross_site_contamination) { |
| url_info_init.WithCrossSitePrefetchContamination(true); |
| } |
| |
| return UrlInfo(url_info_init); |
| } |
| |
| const GURL& NavigationRequest::GetOriginalRequestURL() { |
| // If this is a loadData navigation we should return the URL used to commit, |
| // even if the navigation went through redirects. This is to preserve the |
| // previous behavior where we use the redirect chain from the renderer to get |
| // the original request URL. When we commit a loadDataWithBaseURL, or a |
| // loadDataAsStringWithBaseUrl navigation, the redirect chain in the renderer |
| // used to only contain the commit URL. |
| if (IsLoadDataWithBaseURL()) |
| return GetURL(); |
| |
| // Otherwise, return the first URL in the redirect chain. If the navigation |
| // is started by a client redirect, this will be the URL of the document that |
| // started the redirect. Otherwise, this will be the first destination URL |
| // of the navigation, before any server redirects. |
| // TODO(crbug.com/40168423): Reconsider the behavior with client |
| // redirects, as all script-initiated navigations are considered client |
| // redirects, which means the client redirect might not always trigger |
| // immediately (or at all, if the navigation depends on user interaction) |
| // if we decide to do a reload with the original URL. |
| DCHECK(!redirect_chain_.empty()); |
| return redirect_chain_[0]; |
| } |
| |
| void NavigationRequest::OnResponseStarted( |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| network::mojom::URLResponseHeadPtr response_head, |
| mojo::ScopedDataPipeConsumerHandle response_body, |
| GlobalRequestID request_id, |
| bool is_download, |
| net::NetworkAnonymizationKey network_anonymization_key, |
| SubresourceLoaderParams subresource_loader_params, |
| EarlyHints early_hints) { |
| receive_response_time_ = base::TimeTicks::Now(); |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::OnResponseStarted", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| ScopedCrashKeys crash_keys(*this); |
| |
| // The |loader_|'s job is finished. It must not call the NavigationRequest |
| // anymore from now. |
| loader_.reset(); |
| if (is_download) |
| RecordDownloadUseCountersPrePolicyCheck(); |
| is_download_ = is_download && download_policy().IsDownloadAllowed(); |
| if (is_download_) |
| RecordDownloadUseCountersPostPolicyCheck(); |
| request_id_ = request_id; |
| |
| DCHECK(IsNavigationStarted()); |
| DCHECK(response_head); |
| DCHECK(response_head->parsed_headers); |
| EnterChildTraceEvent("OnResponseStarted", this); |
| SetState(WILL_PROCESS_RESPONSE); |
| response_head_ = std::move(response_head); |
| response_body_ = std::move(response_body); |
| ssl_info_ = response_head_->ssl_info; |
| auth_challenge_info_ = response_head_->auth_challenge_info; |
| |
| // TODO(crbug.com/40218207): Store the whole EarlyHints struct instead |
| // of duplicating all of its fields. |
| was_resource_hints_received_ = early_hints.was_resource_hints_received; |
| early_hints_manager_ = std::move(early_hints.manager); |
| if (early_hints_manager_ && |
| early_hints_manager_->first_early_hints_receive_time()) { |
| base::UmaHistogramTimes( |
| "Navigation.EarlyHints.WillStartRequestToEarlyHintsTime", |
| *early_hints_manager_->first_early_hints_receive_time() - |
| will_start_request_time_); |
| base::UmaHistogramTimes( |
| "Navigation.EarlyHints.EarlyHintsToResponseStartTime", |
| base::TimeTicks::Now() - |
| *early_hints_manager_->first_early_hints_receive_time()); |
| } |
| |
| // A request was made. Record it before we decide to block this response for |
| // a reason or another. |
| RecordAddressSpaceFeature(); |
| |
| const bool is_mhtml_archive = IsMhtmlMimeType(response_head_->mime_type); |
| if (is_mhtml_archive) |
| is_mhtml_or_subframe_ = true; |
| |
| if (CheckCSPEmbeddedEnforcement() == |
| CSPEmbeddedEnforcementResult::BLOCK_RESPONSE) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_BLOCKED_BY_CSP), |
| true /* skip_throttles */, std::nullopt /* error_page_content*/, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // See https://github.com/whatwg/fetch/pull/1579 |
| if (!response_head_->timing_allow_passed) { |
| commit_params_->navigation_timing->parent_resource_timing_access = |
| blink::mojom::ParentResourceTimingAccess::kDoNotReport; |
| } |
| |
| { |
| const std::optional<network::mojom::BlockedByResponseReason> |
| coop_requires_blocking = |
| coop_status_.SanitizeResponse(response_head_.get()); |
| if (coop_requires_blocking) { |
| // TODO(crbug.com/40166503): Investigate what must be done in case |
| // of a download. |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(*coop_requires_blocking), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| policy_container_builder_->SetCrossOriginOpenerPolicy( |
| response_head_->parsed_headers->cross_origin_opener_policy); |
| } |
| |
| ComputePoliciesToCommit(); |
| // After this line. The sandbox flags to commit have been computed. The origin |
| // can be determined. This is needed for enforcing COOP below. |
| |
| { |
| const url::Origin origin = GetOriginForURLLoaderFactoryBeforeResponse( |
| policy_container_builder_->FinalPolicies().sandbox_flags); |
| coop_status_.EnforceCOOP( |
| policy_container_builder_->FinalPolicies().cross_origin_opener_policy, |
| origin, network_anonymization_key); |
| } |
| |
| // The navigation may have encountered a header that requests isolation for |
| // the url's origin. Before we pick the renderer, make sure we update the |
| // origin-isolation opt-ins appropriately. |
| CheckForIsolationOptIn(GetURL()); |
| |
| const bool is_first_response = commit_params_->redirects.empty(); |
| UpdateNavigationHandleTimingsOnResponseReceived(/*is_redirect=*/false, |
| is_first_response); |
| |
| commit_params_->http_response_code = |
| response_head_->headers ? response_head_->headers->response_code() |
| : -1 /* no http_response_code */; |
| |
| // Update fetch start timing. While NavigationRequest updates fetch start |
| // timing for redirects, it's not aware of service worker interception so |
| // fetch start timing could happen earlier than worker start timing. Use |
| // worker ready time if it is greater than the current value to make sure |
| // fetch start timing always comes after worker start timing (if a service |
| // worker intercepted the navigation). |
| commit_params_->navigation_timing->fetch_start = |
| std::max(commit_params_->navigation_timing->fetch_start, |
| response_head_->load_timing.service_worker_ready_time); |
| |
| // A navigation is user activated if it contains a user gesture or the frame |
| // received a gesture and the navigation is renderer initiated. If the |
| // navigation is browser initiated, it has to come from the context menu. |
| // In all cases, the previous and new URLs have to match the |
| // `ShouldPropagateUserActivation` requirements (same eTLD+1). |
| // There are two different checks: |
| // 1. if the `frame_tree_node_` has an origin and is following the rules above |
| // with the target URL, it is used and the bit is set if the navigation is |
| // renderer initiated and the `frame_tree_node_` had a gesture. This should |
| // apply to same page navigations and is preferred over using the referrer |
| // as it can be changed. |
| // 2. if referrer and the target url are following the rules above, two |
| // conditions will set the bit: navigation comes from a gesture and is |
| // renderer initiated (middle click/ctrl+click) or it is coming from a |
| // context menu. This should apply to pages that open in a new tab and we |
| // have to follow the referrer. It means that the activation might not be |
| // transmitted if it should have. |
| if (commit_params_->was_activated == |
| blink::mojom::WasActivatedOption::kUnknown) { |
| commit_params_->was_activated = blink::mojom::WasActivatedOption::kNo; |
| |
| if (!commit_params_->is_browser_initiated && |
| (frame_tree_node_->HasStickyUserActivation() || |
| frame_tree_node_->has_received_user_gesture_before_nav()) && |
| ShouldPropagateUserActivation( |
| frame_tree_node_->current_origin(), |
| url::Origin::Create(common_params_->url))) { |
| commit_params_->was_activated = blink::mojom::WasActivatedOption::kYes; |
| // TODO(crbug.com/41367031): the next check is relying on |
| // sanitized_referrer_ but should ideally use a more reliable source for |
| // the originating URL when the navigation is renderer initiated. |
| } else if (((common_params_->has_user_gesture && |
| !commit_params_->is_browser_initiated) || |
| common_params_->started_from_context_menu) && |
| ShouldPropagateUserActivation( |
| url::Origin::Create(sanitized_referrer_->url), |
| url::Origin::Create(common_params_->url))) { |
| commit_params_->was_activated = blink::mojom::WasActivatedOption::kYes; |
| } |
| } |
| |
| // Check if the response should be sent to a renderer. |
| // Regular downloads should not be rendered, but downloads with an |
| // unsuccessful response code will cause an error page to be rendered. |
| response_should_be_rendered_ = |
| (!is_download || |
| IsFailedDownload(is_download, response_head_->headers.get())) && |
| (!response_head_->headers.get() || |
| (response_head_->headers->response_code() != net::HTTP_NO_CONTENT && |
| response_head_->headers->response_code() != net::HTTP_RESET_CONTENT && |
| !ShouldRenderFallbackContentForResponse(*response_head_->headers))); |
| |
| if (!response_should_be_rendered_) { |
| net_error_ = net::ERR_ABORTED; |
| extended_error_code_ = |
| static_cast<int32_t>(ErrorNavigationTrigger::kShouldNotRenderResponse); |
| SelectFrameHostForOnResponseStarted(std::move(url_loader_client_endpoints), |
| is_download, |
| std::move(subresource_loader_params)); |
| return; |
| } |
| |
| // MHTML document can't be framed into non-MHTML document (and vice versa). |
| // The full page must load from the MHTML archive or none of it. |
| if (is_mhtml_archive && !IsInMainFrame()) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_BLOCKED_BY_RESPONSE), |
| false /* skip_throttles */, std::nullopt /* error_page_contnet */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| const std::optional<network::mojom::BlockedByResponseReason> |
| coep_requires_blocking = EnforceCOEP(); |
| if (coep_requires_blocking) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(*coep_requires_blocking), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| const auto& url = common_params_->url; |
| |
| if (IsDisabledEmbedderInitiatedFencedFrameNavigation()) { |
| frame_tree_node_->current_frame_host()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Embedder-initiated navigations of fenced frames are not allowed after " |
| "both the embedder and embedded fenced frame network access has been " |
| "disabled."); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kFencedFrameEmbedderInitiatedNavigation); |
| OnRequestFailedInternal(completion_status, |
| /*skip_throttles=*/false, |
| /*error_page_content=*/std::nullopt, |
| /*collapse_frame=*/false); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // The fenced frame root and the nested iframes are required to have the |
| // Supports-Loading-Mode HTTP response header "fenced-frame" to be able to |
| // load. Otherwise a console error is emitted. |
| const bool should_enforce_fenced_frame_opt_in = |
| response_head_->headers && frame_tree_node_->IsInFencedFrameTree() && |
| !(url.IsAboutBlank() || url.SchemeIsBlob() || |
| url.SchemeIs(url::kDataScheme)); |
| if (should_enforce_fenced_frame_opt_in && |
| !IsOptedInFencedFrame(*response_head_->headers)) { |
| blink::RecordFencedFrameCreationOutcome( |
| blink::FencedFrameCreationOutcome::kResponseHeaderNotOptIn); |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Supports-Loading-Mode HTTP response header 'fenced-frame' is required " |
| "to load the fenced frame root and its nested iframes."); |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_BLOCKED_BY_RESPONSE), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // [spec]: https://html.spec.whatwg.org/C/#process-a-navigate-response |
| // 4. if [...] the result of checking a navigation response's adherence to its |
| // embedder policy [...], then set failure to true. |
| if (!CheckResponseAdherenceToCoep(url)) { |
| OnRequestFailedInternal(network::URLLoaderCompletionStatus( |
| network::mojom::BlockedByResponseReason:: |
| kCoepFrameResourceNeedsCoepHeader), |
| false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| SelectFrameHostForOnResponseStarted(std::move(url_loader_client_endpoints), |
| is_download, |
| std::move(subresource_loader_params)); |
| } |
| |
| void NavigationRequest::SelectFrameHostForOnResponseStarted( |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| bool is_download, |
| SubresourceLoaderParams subresource_loader_params) { |
| TRACE_EVENT_WITH_FLOW0( |
| "navigation", "NavigationRequest::SelectFrameHostForOnResponseStarted", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| CHECK(!HasRenderFrameHost()) |
| << "`render_frame_host_` should not be set before the " |
| "`NavigationRequest` starts to select the RFH."; |
| ScopedCrashKeys crash_keys(*this); |
| std::string rfh_selected_reason; |
| |
| // Select an appropriate renderer to commit the navigation. |
| if (IsServedFromBackForwardCache()) { |
| NavigationControllerImpl* controller = GetNavigationController(); |
| auto entry = |
| controller->GetBackForwardCache().GetOrEvictEntry(nav_entry_id_); |
| if (!entry.has_value() && |
| entry.error() == BackForwardCacheImpl::kEntryIneligibleAndEvicted) { |
| // If the RenderFrameHost to restore has been evicted and deleted, or the |
| // current navigation is being restarted due to the `GetOrEvictEntry` |
| // call, we should stop processing this back/forward cache restore |
| // navigation, as the navigation will soon be restarted as a normal |
| // history navigation and the current NavigationRequest will be reset. |
| // DO NOT ADD CODE after this. The previous call to |
| // `GetOrEvictEntry()` has destroyed the NavigationRequest. |
| return; |
| } |
| CHECK(entry.has_value() && entry.value()); |
| CHECK(entry.value()->render_frame_host()); |
| render_frame_host_ = entry.value()->render_frame_host()->GetSafeRef(); |
| } else if (IsPrerenderedPageActivation()) { |
| // Prerendering requires changing pages starting at the root node. |
| DCHECK(IsInMainFrame()); |
| |
| render_frame_host_ = GetPrerenderHostRegistry() |
| .GetRenderFrameHostForReservedHost( |
| prerender_frame_tree_node_id_.value()) |
| ->GetSafeRef(); |
| } else if (response_should_be_rendered_) { |
| std::string* reason_output = |
| base::FeatureList::IsEnabled( |
| features::kHoldbackDebugReasonStringRemoval) |
| ? &rfh_selected_reason |
| : nullptr; |
| |
| if (auto result = |
| frame_tree_node_->render_manager()->GetFrameHostForNavigation( |
| this, &browsing_context_group_swap_, |
| ProcessAllocationContext::CreateForNavigationRequest( |
| ProcessAllocationNavigationStage::kAfterResponse, |
| navigation_id_), |
| reason_output); |
| result.has_value()) { |
| render_frame_host_ = result.value()->GetSafeRef(); |
| } else { |
| switch (result.error()) { |
| case GetFrameHostForNavigationFailed::kCouldNotReinitializeMainFrame: |
| // TODO(crbug.com/40250311): This was unhandled before and |
| // remains explicitly unhandled. This branch may be removed in the |
| // future. |
| break; |
| case GetFrameHostForNavigationFailed::kBlockedByPendingCommit: |
| DCHECK(ShouldQueueDueToExistingPendingCommitRFH()); |
| // This closure is posted to the event loop, so it must use WeakPtr. |
| resume_commit_closure_ = base::BindOnce( |
| &NavigationRequest::SelectFrameHostForOnResponseStarted, |
| weak_factory_.GetWeakPtr(), |
| std::move(url_loader_client_endpoints), is_download, |
| std::move(subresource_loader_params)); |
| frame_tree_node_->render_manager() |
| ->speculative_frame_host() |
| ->RecordMetricsForBlockedGetFrameHostAttempt( |
| /* commit_attempt=*/true); |
| return; |
| } |
| } |
| |
| // GetFrameHostForNavigation() should update associated_rfh_type_, so it |
| // should never be NONE here. |
| DCHECK_NE(AssociatedRenderFrameHostType::NONE, associated_rfh_type_); |
| |
| CHECK(Navigator::CheckWebUIRendererDoesNotDisplayNormalURL( |
| GetRenderFrameHost(), GetUrlInfo(), |
| /* is_renderer_initiated_check */ false)); |
| } else { |
| render_frame_host_ = std::nullopt; |
| } |
| if (!HasRenderFrameHost()) { |
| DCHECK(!response_should_be_rendered_); |
| } |
| |
| if (!commit_params_->is_browser_initiated && HasRenderFrameHost() && |
| GetRenderFrameHost()->GetProcess() != |
| frame_tree_node_->current_frame_host()->GetProcess()) { |
| // Allow the embedder to cancel the cross-process commit if needed. |
| if (!frame_tree_node_->navigator() |
| .GetDelegate() |
| ->ShouldAllowRendererInitiatedCrossProcessNavigation( |
| frame_tree_node_->IsOutermostMainFrame())) { |
| net_error_ = net::ERR_ABORTED; |
| extended_error_code_ = static_cast<int32_t>( |
| ErrorNavigationTrigger:: |
| kRenderInitiatedCrossProcessNavigationNotAllowed); |
| frame_tree_node_->ResetNavigationRequest( |
| NavigationDiscardReason::kInternalCancellation); |
| return; |
| } |
| } |
| |
| // Store the URLLoaderClient endpoints until checks have been processed. |
| url_loader_client_endpoints_ = std::move(url_loader_client_endpoints); |
| |
| subresource_loader_params_ = std::move(subresource_loader_params); |
| |
| // Most cases where ShouldAssignSiteForUrlInfo() is false should never load |
| // actual content and reach this. Since only empty document schemes are |
| // allowed to leave a SiteInstance's site unassigned, they should follow the |
| // !NeedsUrlLoader() path for committing the navigation early without ever |
| // making a network request, and hence they should never reach the response |
| // processing code here. |
| // |
| // The sole exception to this is about:blank URLs, since extensions are |
| // allowed to redirect to them after a regular network request/response has |
| // started. Hence, about:blank is the only possible URL which both uses |
| // unassigned SiteInstances and can reach this point (via an extension |
| // redirect). |
| if (common_params_->url.IsAboutBlank()) { |
| // TODO(alexmos): Convert to a CHECK after verifying that this doesn't |
| // happen in practice. |
| if (!WasServerRedirect()) { |
| DVLOG(1) << "about:blank should only go through the network stack " |
| << "when an extension redirects to it."; |
| base::debug::DumpWithoutCrashing(); |
| } |
| } else { |
| // TODO(alexmos): Convert to a CHECK after verifying that this doesn't |
| // happen in practice. |
| if (!SiteInstanceImpl::ShouldAssignSiteForUrlInfo(GetUrlInfo())) { |
| DVLOG(1) << "This URL was unexpectedly loaded through the network stack: " |
| << common_params_->url; |
| base::debug::DumpWithoutCrashing(); |
| } |
| } |
| |
| if (HasRenderFrameHost()) { |
| // Set the site URL now if it hasn't been set already. If the site requires |
| // a dedicated process, this will lock the process to that site, which will |
| // prevent other sites from incorrectly reusing this process. See |
| // https://crbug.com/738634. |
| SiteInstanceImpl* instance = GetRenderFrameHost()->GetSiteInstance(); |
| if (!instance->HasSite() && |
| SiteInstanceImpl::ShouldAssignSiteForUrlInfo(GetUrlInfo())) { |
| instance->ConvertToDefaultOrSetSite(GetUrlInfo()); |
| } |
| |
| // Since we've made the final pick for the RenderFrameHost above, the picked |
| // RenderFrameHost's process should be considered "tainted" for future |
| // process reuse decisions. That is, a site requiring a dedicated process |
| // should not reuse this process, unless it's same-site with the URL we're |
| // committing. |
| // |
| // The process must be marked used after calling ConvertToDefaultOrSetSite, |
| // because that call verifies that a SiteInstance with an unassigned site |
| // (e.g., about:blank) can only be locked to a site if it is still unused. |
| // |
| // Note that although NavigationThrottles could still cancel the navigation |
| // as part of WillProcessResponse below, we must update the process here, |
| // since otherwise there could be a race if a NavigationThrottle defers the |
| // navigation, and in the meantime another navigation reads the incorrect |
| // IsUnused() value from the same process when making a process reuse |
| // decision. |
| GetRenderFrameHost()->GetProcess()->SetIsUsed(); |
| |
| // Now that we know the IsolationContext for the assigned SiteInstance, we |
| // opt the origin into OAC here if needed. Note that this doesn't need to |
| // account for loading data URLs with a base URL, because such a base URL |
| // can never opt into OAC. |
| // TODO(wjmaclean): Remove this call/function when same-process |
| // OriginAgentCluster moves to SiteInstanceGroup, as then all OAC origins |
| // will get a SiteInstance (regardless of process isolation) and tracking |
| // will be handled by the existing pathway in |
| // SiteInstanceImpl::SetSiteInfoInternal(). |
| const IsolationContext& isolation_context = instance->GetIsolationContext(); |
| AddOriginAgentClusterStateIfNecessary(isolation_context); |
| |
| // TODO(wjmaclean): Once this is all working, consider combining the |
| // following code into the function above. |
| // If this navigation request didn't opt-in to origin isolation, we need |
| // to check here in case the origin has previously requested isolation and |
| // should be marked as opted-out in this SiteInstance. At this point we know |
| // that |render_frame_host_|'s SiteInstance has been finalized, so it's safe |
| // to use it here to get the correct |IsolationContext|. |
| // |
| // When loading a data URL with a base URL, use the base URL to calculate |
| // the origin; otherwise, `AddDefaultIsolatedOriginIfNeeded()` will simply |
| // do nothing as a data: URL has an opaque origin. |
| // |
| // TODO(wjmaclean): this won't handle cases like about:blank (where it |
| // inherits an origin we care about). We plan to compute the origin |
| // before commit time (https://crbug.com/888079), which may make it |
| // possible to compute the right origin here. |
| const url::Origin origin = |
| IsLoadDataWithBaseURL() |
| ? url::Origin::Create(common_params_->base_url_for_data_url) |
| : url::Origin::Create(common_params_->url); |
| ChildProcessSecurityPolicyImpl::GetInstance() |
| ->AddDefaultIsolatedOriginIfNeeded( |
| isolation_context, origin, |
| false /* is_global_walk_or_frame_removal */); |
| |
| // Replace the SiteInstance of the previously committed entry if it's for a |
| // url that doesn't require a site assignment, if this new commit will be |
| // assigning an incompatible site to the previous SiteInstance. This ensures |
| // the new SiteInstance can be used with the old entry if we return to it. |
| // See http://crbug.com/992198 for further context. |
| NavigationEntryImpl* nav_entry = |
| frame_tree_node_->navigator().controller().GetLastCommittedEntry(); |
| if (nav_entry && !nav_entry->GetURL().IsAboutBlank() && |
| !SiteInstance::ShouldAssignSiteForURL(nav_entry->GetURL()) && |
| SiteInstanceImpl::ShouldAssignSiteForUrlInfo(GetUrlInfo())) { |
| scoped_refptr<FrameNavigationEntry> frame_entry = |
| nav_entry->root_node()->frame_entry; |
| scoped_refptr<SiteInstanceImpl> new_site_instance = |
| base::WrapRefCounted<SiteInstanceImpl>(static_cast<SiteInstanceImpl*>( |
| instance->GetRelatedSiteInstance(frame_entry->url()).get())); |
| nav_entry->AddOrUpdateFrameEntry( |
| frame_tree_node_, NavigationEntryImpl::UpdatePolicy::kReplace, |
| frame_entry->item_sequence_number(), |
| frame_entry->document_sequence_number(), |
| frame_entry->navigation_api_key(), new_site_instance.get(), |
| frame_entry->source_site_instance(), frame_entry->url(), |
| frame_entry->committed_origin(), frame_entry->referrer(), |
| frame_entry->initiator_origin(), frame_entry->initiator_base_url(), |
| frame_entry->redirect_chain(), frame_entry->page_state(), |
| frame_entry->method(), frame_entry->post_id(), |
| frame_entry->blob_url_loader_factory(), |
| frame_entry->policy_container_policies() |
| ? frame_entry->policy_container_policies()->ClonePtr() |
| : nullptr); |
| } |
| } |
| |
| devtools_instrumentation::OnNavigationResponseReceived(*this, |
| *response_head_); |
| |
| // The response code indicates that this is an error page, but we don't |
| // know how to display the content. We follow Firefox here and show our |
| // own error page instead of intercepting the request as a stream or a |
| // download. |
| if (IsFailedDownload(is_download, response_head_->headers.get())) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(net::ERR_INVALID_RESPONSE), |
| false /* skip_throttles */, std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| net::Error net_error = CheckContentSecurityPolicy( |
| was_redirected_ /* has_followed_redirect */, |
| false /* url_upgraded_after_redirect */, true /* is_response_check */); |
| DCHECK_NE(net_error, net::ERR_BLOCKED_BY_CLIENT); |
| if (net_error != net::OK) { |
| OnRequestFailedInternal(network::URLLoaderCompletionStatus(net_error), |
| false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // TODO(crbug.com/399783247): Remove |
| if (base::FeatureList::IsEnabled( |
| features::kHoldbackDebugReasonStringRemoval)) { |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "base_host_for_data_url", |
| common_params_->base_url_for_data_url.host_piece()); |
| SCOPED_CRASH_KEY_STRING1024("Bug1454273", "rfh_selected_reason", |
| rfh_selected_reason); |
| } |
| |
| if (HasRenderFrameHost() && |
| !CheckPermissionsPoliciesForFencedFrames(GetOriginToCommit().value())) { |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kFencedFramesPermissionPolicyBlocked); |
| OnRequestFailedInternal(completion_status, false /*skip_throttles*/, |
| std::nullopt /*error_page_content*/, |
| false /*collapse_frame*/); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // Check if the navigation should be allowed to proceed. |
| WillProcessResponse(); |
| } |
| |
| void NavigationRequest::OnRequestFailed( |
| const network::URLLoaderCompletionStatus& status) { |
| DCHECK_NE(status.error_code, net::OK); |
| |
| OnRequestFailedInternal( |
| status, false /* skip_throttles */, std::nullopt /* error_page_content */, |
| status.should_collapse_initiator /* collapse_frame */); |
| } |
| |
| std::optional<NavigationEarlyHintsManagerParams> |
| NavigationRequest::CreateNavigationEarlyHintsManagerParams( |
| const network::mojom::EarlyHints& early_hints) { |
| TRACE_EVENT_WITH_FLOW0( |
| "navigation", |
| "NavigationRequest::CreateNavigationEarlyHintsManagerParams", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| // Early Hints preloads should happen only before the final response is |
| // received, and limited only in the main frame for now. |
| CHECK(!HasRenderFrameHost()); |
| CHECK(loader_); |
| CHECK_LT(state_, WILL_PROCESS_RESPONSE); |
| CHECK(!IsSameDocument()); |
| CHECK(IsInMainFrame()); |
| DCHECK(!IsPageActivation()); |
| |
| // Getting a RenderProcessHost from a tentative RenderFrameHost during |
| // navigation is generally discouraged because it has potential performance |
| // impact (the RenderProcessHost could be discarded without actually being |
| // used after a cross origin redirect). However, Early Hints preloads require |
| // the RenderProcessHost for the tentative RenderFrameHost to set up |
| // URLLoaderFactoryParams accordingly. The performance implication should be |
| // negligible for Early Hints because these are rarely followed by cross |
| // origin redirects. Early Hints preloads before a cross origin redirect don't |
| // make sense since such preloads are not available for the redirected page. |
| // The CrossOriginRedirectAfterEarlyHints variant of |
| // Navigation.MainFrame.TimeToReadyToCommit2 histogram tracks the performance |
| // impacts. |
| auto result = frame_tree_node_->render_manager()->GetFrameHostForNavigation( |
| this, &browsing_context_group_swap_, |
| ProcessAllocationContext::CreateForNavigationRequest( |
| ProcessAllocationNavigationStage::kHandlingEarlyHints, |
| navigation_id_)); |
| |
| // Early hints is an optimization; if it is not possible to get a suitable |
| // RenderFrameHost for any reason, just bail out. |
| if (!result.has_value()) { |
| return std::nullopt; |
| } |
| |
| RenderProcessHost* process = result.value()->GetProcess(); |
| |
| // The process is shutting down. |
| if (!process->GetBrowserContext()) |
| return std::nullopt; |
| |
| // Compute sandbox flags. Currently just inherit from the frame. |
| // TODO(crbug.com/40188470): Think about the right way the specification |
| // should handle sandbox flags with Early Hints. |
| network::mojom::WebSandboxFlags sandbox_flags = |
| commit_params_->frame_policy.sandbox_flags; |
| |
| const url::Origin tentative_origin = |
| GetOriginForURLLoaderFactoryBeforeResponse(sandbox_flags); |
| |
| mojo::PendingRemote<network::mojom::CookieAccessObserver> cookie_observer; |
| cookie_observers_->Add(cookie_observer.InitWithNewPipeAndPassReceiver(), |
| CookieAccessDetails::Source::kNonNavigation); |
| |
| mojo::PendingRemote<network::mojom::TrustTokenAccessObserver> |
| trust_token_observer; |
| Clone(trust_token_observer.InitWithNewPipeAndPassReceiver()); |
| |
| mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver> |
| shared_dictionary_observer; |
| Clone(shared_dictionary_observer.InitWithNewPipeAndPassReceiver()); |
| |
| mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver> |
| device_bound_session_observer; |
| Clone(device_bound_session_observer.InitWithNewPipeAndPassReceiver()); |
| |
| network::mojom::URLLoaderFactoryParamsPtr url_loader_factory_params = |
| URLLoaderFactoryParamsHelper::CreateForEarlyHintsPreload( |
| process, tentative_origin, *this, early_hints, |
| std::move(cookie_observer), std::move(trust_token_observer), |
| std::move(shared_dictionary_observer), |
| std::move(device_bound_session_observer)); |
| |
| net::IsolationInfo isolation_info = url_loader_factory_params->isolation_info; |
| |
| // TODO(crbug.com/40188470): Support DevTools instrumentation and extension's |
| // WebRequest API. |
| auto loader_factory = url_loader_factory::CreatePendingRemote( |
| ContentBrowserClient::URLLoaderFactoryType::kEarlyHints, |
| url_loader_factory::TerminalParams::ForNetworkContext( |
| process->GetStoragePartition()->GetNetworkContext(), |
| std::move(url_loader_factory_params))); |
| |
| did_create_early_hints_manager_params_ = true; |
| return NavigationEarlyHintsManagerParams( |
| tentative_origin, std::move(isolation_info), |
| mojo::Remote<network::mojom::URLLoaderFactory>( |
| std::move(loader_factory))); |
| } |
| |
| void NavigationRequest::OnRequestFailedInternal( |
| const network::URLLoaderCompletionStatus& status, |
| bool skip_throttles, |
| const std::optional<std::string>& error_page_content, |
| bool collapse_frame) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::OnRequestFailedInternal", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| CheckStateTransition(WILL_FAIL_REQUEST); |
| DCHECK(!(status.error_code == net::ERR_ABORTED && |
| error_page_content.has_value())); |
| ScopedCrashKeys crash_keys(*this); |
| |
| if (prerender_frame_tree_node_id_.has_value() && |
| !prerender_frame_tree_node_id_.value().is_null()) { |
| // Prerender activation must not fail but some reports imply it can actually |
| // be failing: crbug.com/411566699, crbug.com/408969974. This dump is useful |
| // for debugging it. |
| PrerenderHostRegistry& registry = GetPrerenderHostRegistry(); |
| std::string prerender_type = GeneratePrerenderHistogramSuffix( |
| registry.GetPrerenderTriggerType(prerender_frame_tree_node_id()), |
| registry.GetPrerenderEmbedderHistogramSuffix( |
| prerender_frame_tree_node_id())); |
| SCOPED_CRASH_KEY_STRING64("Bug411566699", "prerender_type", prerender_type); |
| base::debug::DumpWithoutCrashing(); |
| } |
| |
| if (MaybeEvictFromBackForwardCacheBySubframeNavigation( |
| frame_tree_node_->current_frame_host())) { |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| return; |
| } |
| |
| // The request failed, the |loader_| must not call the NavigationRequest |
| // anymore from now while the error page is being loaded. |
| loader_.reset(); |
| |
| // Reset the RenderFrameHost R1 that had been computed for committing the |
| // failed navigation. This breaks the binding between the current |
| // NavigationRequest and R1, so that if we create another speculative |
| // RenderFrameHost R2 to commit an error page after this, deleting R1 won't |
| // try to delete this NavigationRequest along with it. |
| render_frame_host_ = std::nullopt; |
| |
| // Do not update ssl_info_ on HTTP_RESPONSE_CODE_FAILURE (e.g., HTTP 4xx/5xx |
| // errors). In these cases, URLLoaderCompletionStatus does not have ssl_info. |
| // The existing ssl_info_ should be preserved by design, so the certificate is |
| // retained for error pages, ensuring correct security UI and metrics. |
| if (status.error_code != net::ERR_HTTP_RESPONSE_CODE_FAILURE) { |
| ssl_info_ = status.ssl_info; |
| } |
| |
| devtools_instrumentation::OnNavigationRequestFailed(*this, status); |
| |
| // TODO(crbug.com/41340435): Check that ssl_info.has_value() if |
| // net_error is a certificate error. |
| EnterChildTraceEvent("OnRequestFailed", this, "error", status.error_code); |
| SetState(WILL_FAIL_REQUEST); |
| processing_navigation_throttle_ = false; |
| |
| // Ensure the pending entry also gets discarded if it has no other active |
| // requests. |
| pending_entry_ref_.reset(); |
| |
| net_error_ = static_cast<net::Error>(status.error_code); |
| extended_error_code_ = status.extended_error_code; |
| resolve_error_info_ = status.resolve_error_info; |
| navigation_handle_timing_.request_failed_time = base::TimeTicks::Now(); |
| |
| if (MaybeCancelFailedNavigation()) |
| return; |
| |
| // Only notify the NavigationHandleTiming update if the navigation will commit |
| // an error page (instead of getting ignored and deleted above). |
| GetDelegate()->DidUpdateNavigationHandleTiming(this); |
| |
| if (collapse_frame) { |
| DCHECK_EQ(net::ERR_BLOCKED_BY_CLIENT, status.error_code); |
| frame_tree_node_->SetCollapsed(true); |
| } |
| |
| is_mhtml_or_subframe_ = false; |
| // TODO(crbug.com/40736932): Apparently, error pages inherit sandbox |
| // flags from their parent/opener. Document loaded from the network |
| // shouldn't have any influence over Chrome's internal error page. We should |
| // define our own flags, preferably the strictest ones instead. |
| ComputePoliciesToCommitForError(); |
| |
| const auto origin = url::Origin(); |
| // Set the COOP origin in the policy container builder before FinalPolicies() |
| // is called. |
| coop_status_.EnforceCOOP( |
| policy_container_builder_->FinalPolicies().cross_origin_opener_policy, |
| origin, net::NetworkAnonymizationKey::CreateTransient()); |
| |
| SelectFrameHostForOnRequestFailedInternal(status.exists_in_cache, |
| skip_throttles, error_page_content); |
| } |
| |
| void NavigationRequest::SelectFrameHostForOnRequestFailedInternal( |
| bool exists_in_cache, |
| bool skip_throttles, |
| const std::optional<std::string>& error_page_content) { |
| CHECK(!HasRenderFrameHost()) |
| << "`render_frame_host_` should not be set before the " |
| "`NavigationRequest` starts to select the RFH."; |
| |
| switch (ComputeErrorPageProcess()) { |
| case ErrorPageProcess::kCurrentProcess: |
| // There's no way to get here with a same-document navigation, it would |
| // need to be on a document that was not blocked but became blocked, but |
| // same document navigations don't go to the network so it wouldn't know |
| // about the change. |
| CHECK(!IsSameDocument()); |
| break; |
| case ErrorPageProcess::kIsolatedProcess: |
| // In this case we are isolating the error page from the source and |
| // destination process, and want it to go to a new process. |
| // |
| // TODO(nasko): Investigate whether GetFrameHostForNavigation can properly |
| // account for clearing the expected process if it clears the speculative |
| // RenderFrameHost. See https://crbug.com/793127. |
| ResetExpectedProcess(); |
| [[fallthrough]]; |
| case ErrorPageProcess::kDestinationProcess: |
| // A same-document navigation would normally attempt to navigate the |
| // current document, but since we will be presenting an error instead and |
| // there will not be a document to navigate. We always make an error here |
| // into a cross-document navigation. See https://crbug.com/1018385 and |
| // https://crbug.com/1125106. |
| common_params_->navigation_type = |
| ConvertToCrossDocumentType(common_params_->navigation_type); |
| break; |
| case ErrorPageProcess::kNotErrorPage: |
| case ErrorPageProcess::kPostCommitErrorPage: |
| NOTREACHED(); |
| } |
| |
| RenderFrameHostImpl* render_frame_host = nullptr; |
| if (auto result = |
| frame_tree_node_->render_manager()->GetFrameHostForNavigation( |
| this, &browsing_context_group_swap_, |
| ProcessAllocationContext::CreateForNavigationRequest( |
| ProcessAllocationNavigationStage::kAfterFailure, |
| navigation_id_)); |
| result.has_value()) { |
| render_frame_host = result.value(); |
| } else { |
| switch (result.error()) { |
| case GetFrameHostForNavigationFailed::kCouldNotReinitializeMainFrame: |
| // TODO(crbug.com/40250311): This was unhandled |
| // before and remains explicitly unhandled. This branch may be |
| // removed in the future. |
| break; |
| case GetFrameHostForNavigationFailed::kBlockedByPendingCommit: |
| resume_commit_closure_ = base::BindOnce( |
| &NavigationRequest::SelectFrameHostForOnRequestFailedInternal, |
| weak_factory_.GetWeakPtr(), exists_in_cache, skip_throttles, |
| error_page_content); |
| frame_tree_node_->render_manager() |
| ->speculative_frame_host() |
| ->RecordMetricsForBlockedGetFrameHostAttempt( |
| /* commit_attempt=*/true); |
| return; |
| } |
| } |
| |
| render_frame_host_ = render_frame_host->GetSafeRef(); |
| |
| // Update the associated RenderFrameHost type. |
| SetAssociatedRFHType( |
| GetRenderFrameHost() == |
| frame_tree_node_->render_manager()->current_frame_host() |
| ? AssociatedRenderFrameHostType::CURRENT |
| : AssociatedRenderFrameHostType::SPECULATIVE); |
| |
| // Set the site URL now if it hasn't been set already. It's possible to get |
| // here if we navigate to an error out of an initial "blank" SiteInstance. |
| // Also mark the process as used, since it will be hosting an error page. |
| SiteInstanceImpl* instance = GetRenderFrameHost()->GetSiteInstance(); |
| if (!instance->HasSite()) |
| instance->ConvertToDefaultOrSetSite(GetUrlInfo()); |
| GetRenderFrameHost()->GetProcess()->SetIsUsed(); |
| |
| // The check for WebUI should be performed only if error page isolation is |
| // enabled for this failed navigation. It is possible for subframe error page |
| // to be committed in a WebUI process as shown in https://crbug.com/944086. |
| if (frame_tree_node_->IsErrorPageIsolationEnabled()) { |
| CHECK(Navigator::CheckWebUIRendererDoesNotDisplayNormalURL( |
| GetRenderFrameHost(), GetUrlInfo(), |
| /* is_renderer_initiated_check */ false)); |
| } |
| |
| has_stale_copy_in_cache_ = exists_in_cache; |
| |
| if (skip_throttles) { |
| // The NavigationHandle shouldn't be notified about renderer-debug URLs. |
| // They will be handled by the renderer process. |
| CommitErrorPage(error_page_content); |
| } else { |
| // Check if the navigation should be allowed to proceed. |
| WillFailRequest(); |
| } |
| } |
| |
| NavigationRequest::ErrorPageProcess |
| NavigationRequest::ComputeErrorPageProcess() { |
| if (net_error_ == net::OK) { |
| return ErrorPageProcess::kNotErrorPage; |
| } |
| |
| if (state_ < NavigationRequest::CANCELING) { |
| CHECK(browser_initiated_error_navigation_type_ != |
| BrowserInitiatedErrorNavigationType::kNone); |
| |
| if (browser_initiated_error_navigation_type_ == |
| BrowserInitiatedErrorNavigationType::kPostCommit) { |
| // Post-commit error page normally goes through the "non-error page" |
| // navigation path, so treat them specially here too. |
| return ErrorPageProcess::kPostCommitErrorPage; |
| } |
| |
| // Otherwise, this is a normal browser-initiated error navigation, which |
| // should fall out of this block and use existing process selection |
| // behavior. |
| } |
| |
| // By policy we can isolate all error pages from both the current and |
| // destination processes. |
| if (frame_tree_node_->IsErrorPageIsolationEnabled()) |
| return ErrorPageProcess::kIsolatedProcess; |
| |
| // Decide whether to leave the error page in the original process. |
| // * If this was a renderer-initiated navigation, and the request is blocked |
| // because the initiating document wasn't allowed to make the request, |
| // commit the error in the existing process. This is a strategy to to |
| // avoid creating a process for the destination, which may belong to an |
| // origin with a higher privilege level. |
| // * Error pages resulting from errors like network outage, no network, or |
| // DNS error can reasonably expect that a reload at a later point in time |
| // would work. These should be allowed to transfer away from the current |
| // process: they do belong to whichever process that will host the |
| // destination URL, as a reload will end up committing in that process |
| // anyway. |
| // * Error pages that arise during browser-initiated navigations to blocked |
| // URLs should be allowed to transfer away from the current process, which |
| // didn't request the navigation and may have a higher privilege level |
| // than the blocked destination. |
| if (net::IsRequestBlockedError(net_error_) && !browser_initiated()) { |
| return ErrorPageProcess::kCurrentProcess; |
| } |
| return ErrorPageProcess::kDestinationProcess; |
| } |
| |
| void NavigationRequest::OnStartChecksComplete( |
| NavigationThrottle::ThrottleCheckResult result) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::OnStartChecksComplete", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| DCHECK(result.action() != NavigationThrottle::DEFER); |
| DCHECK(result.action() != NavigationThrottle::BLOCK_RESPONSE); |
| |
| if (on_start_checks_complete_closure_) |
| std::move(on_start_checks_complete_closure_).Run(); |
| // Abort the request if needed. This will destroy the NavigationRequest. |
| if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::CANCEL || |
| result.action() == NavigationThrottle::BLOCK_REQUEST || |
| result.action() == NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE) { |
| #if DCHECK_IS_ON() |
| if (result.action() == NavigationThrottle::BLOCK_REQUEST) { |
| DCHECK(net::IsRequestBlockedError(result.net_error_code())); |
| } else if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE) { |
| // TODO(clamy): distinguish between CANCEL and CANCEL_AND_IGNORE. |
| DCHECK_EQ(result.net_error_code(), net::ERR_ABORTED); |
| } |
| #endif |
| |
| bool collapse_frame = |
| result.action() == NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE; |
| |
| // If the start checks completed synchronously, which could happen if there |
| // is no onbeforeunload handler or if a NavigationThrottle cancelled it, |
| // then this could cause reentrancy into NavigationController. So use a |
| // PostTask to avoid that. |
| auto completion_status = |
| network::URLLoaderCompletionStatus(result.net_error_code()); |
| if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::CANCEL) { |
| completion_status.extended_error_code = |
| static_cast<int>(ErrorNavigationTrigger::kNavigationThrottleCancel); |
| } else { |
| completion_status.extended_error_code = |
| static_cast<int>(ErrorNavigationTrigger::kNavigationThrottleBlock); |
| } |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&NavigationRequest::OnRequestFailedInternal, |
| weak_factory_.GetWeakPtr(), std::move(completion_status), |
| true /* skip_throttles */, result.error_page_content(), |
| collapse_frame)); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| StoragePartition* partition = GetStoragePartitionWithCurrentSiteInfo(); |
| DCHECK(partition); |
| |
| // |loader_| should not exist if the service worker handle |
| // will be destroyed, since it holds raw pointers to it. See the |
| // comment in the header for |loader_|. |
| DCHECK(!loader_); |
| |
| // Only initialize the ServiceWorkerMainResourceHandle if it can be created |
| // for this frame. |
| bool can_create_service_worker = |
| (frame_tree_node_->pending_frame_policy().sandbox_flags & |
| network::mojom::WebSandboxFlags::kOrigin) != |
| network::mojom::WebSandboxFlags::kOrigin; |
| if (can_create_service_worker) { |
| ServiceWorkerContextWrapper* service_worker_context = |
| static_cast<ServiceWorkerContextWrapper*>( |
| partition->GetServiceWorkerContext()); |
| std::string fetch_event_client_id; |
| // TODO(crbug.com/368087661): According to the spec this should be fetch |
| // request's client but it is not here in the implementation. |
| if (auto client = frame_tree_node_->current_frame_host() |
| ->GetLastCommittedServiceWorkerClient()) { |
| fetch_event_client_id = client->client_uuid(); |
| } |
| service_worker_handle_ = std::make_unique<ServiceWorkerMainResourceHandle>( |
| service_worker_context, |
| base::BindRepeating(&NavigationRequest::OnServiceWorkerAccessed, |
| weak_factory_.GetWeakPtr()), |
| std::move(fetch_event_client_id)); |
| } |
| |
| // Mark the fetch_start (Navigation Timing API). |
| commit_params_->navigation_timing->fetch_start = base::TimeTicks::Now(); |
| |
| // Ensure that normal history navigations can dispatch the Navigation API's |
| // navigate event as the navigation is starting. Cases without a UrlLoader |
| // are handled in OnWillCommitWithoutUrlLoaderChecksComplete. |
| MaybeDispatchNavigateEventForCrossDocumentTraversal(); |
| |
| std::unique_ptr<NavigationUIData> navigation_ui_data; |
| if (navigation_ui_data_) |
| navigation_ui_data = navigation_ui_data_->Clone(); |
| |
| // Give DevTools a chance to override begin params (headers, skip SW) |
| // before actually loading resource. |
| bool report_raw_headers = false; |
| std::optional<std::vector<net::SourceStreamType>> |
| devtools_accepted_stream_types; |
| devtools_instrumentation::ApplyNetworkRequestOverrides( |
| frame_tree_node_, begin_params_.get(), &report_raw_headers, |
| &devtools_accepted_stream_types, &devtools_user_agent_override_, |
| &devtools_accept_language_override_); |
| devtools_instrumentation::OnNavigationRequestWillBeSent(*this); |
| |
| // Merge headers with embedder's headers. |
| net::HttpRequestHeaders headers; |
| headers.AddHeadersFromString(begin_params_->headers); |
| headers.MergeFrom(TakeModifiedRequestHeaders()); |
| begin_params_->headers = headers.ToString(); |
| |
| // TODO(clamy): Avoid cloning the navigation params and create the |
| // ResourceRequest directly here. |
| std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptor; |
| net::HttpRequestHeaders cors_exempt_headers; |
| std::swap(cors_exempt_headers, cors_exempt_request_headers_); |
| |
| auto loader_type = NavigationURLLoader::LoaderType::kRegular; |
| network::mojom::URLResponseHeadPtr cached_response_head = nullptr; |
| if (IsServedFromBackForwardCache()) { |
| loader_type = NavigationURLLoader::LoaderType::kNoopForBackForwardCache; |
| cached_response_head = GetRenderFrameHostRestoredFromBackForwardCache() |
| ->last_response_head() |
| ->Clone(); |
| } else if (IsPrerenderedPageActivation()) { |
| loader_type = NavigationURLLoader::LoaderType::kNoopForPrerender; |
| DCHECK(prerender_frame_tree_node_id_.has_value()); |
| const network::mojom::URLResponseHeadPtr& last_response_head = |
| GetPrerenderHostRegistry() |
| .GetRenderFrameHostForReservedHost(*prerender_frame_tree_node_id_) |
| ->last_response_head(); |
| // As PrerenderCommitDeferringCondition makes sure to finish the prerender |
| // initial navigation before activation, a valid last_response_head should |
| // be always stored before reaching here. |
| DCHECK(last_response_head); |
| cached_response_head = last_response_head->Clone(); |
| } |
| |
| // Sandbox flags inherited from the frame. In particular, this does not |
| // include: |
| // - Sandbox flags inherited from the creator via the PolicyContainer. |
| // - Sandbox flags forced for MHTML documents. |
| // - Sandbox flags from the future response via CSP. |
| // It is used by the ExternalProtocolHandler to ensure sandboxed iframe won't |
| // navigate the user toward a different application, which can be seen as a |
| // main frame navigation somehow. |
| network::mojom::WebSandboxFlags sandbox_flags = |
| commit_params_->frame_policy.sandbox_flags; |
| |
| // Reset the compositor lock before starting the loader. |
| compositor_lock_.reset(); |
| |
| BrowserContext* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| |
| // Create `PrefetchServingPageMetricsContainer` only if the initiator |
| // document has its `PrefetchDocumentManager`. |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container; |
| if (!IsSameDocument() && initiator_document_token_ && |
| PrefetchDocumentManager::FromDocumentToken(initiator_process_id_, |
| *initiator_document_token_)) { |
| serving_page_metrics_container = |
| PrefetchServingPageMetricsContainer::GetOrCreateForNavigationHandle( |
| *this) |
| ->GetWeakPtr(); |
| } |
| |
| loader_ = NavigationURLLoader::Create( |
| browser_context, partition, |
| std::make_unique<NavigationRequestInfo>( |
| common_params_->Clone(), begin_params_.Clone(), sandbox_flags, |
| GetIsolationInfo(), |
| frame_tree_node_->current_frame_host()->IsInPrimaryMainFrame(), |
| frame_tree_node_->IsOutermostMainFrame(), |
| frame_tree_node_->IsMainFrame(), |
| frame_tree_node_->AreAncestorsSecure(), |
| frame_tree_node_->frame_tree_node_id(), report_raw_headers, |
| upgrade_if_insecure_, |
| blob_url_loader_factory_ ? blob_url_loader_factory_->Clone() |
| : nullptr, |
| devtools_navigation_token(), |
| frame_tree_node_->current_frame_host()->devtools_frame_token(), |
| std::move(cors_exempt_headers), |
| BuildClientSecurityStateForNavigationFetch(), |
| devtools_accepted_stream_types, is_pdf_, GetInitiatorProcessId(), |
| initiator_document_token_, GetPreviousRenderFrameHostId(), |
| std::move(serving_page_metrics_container), |
| allow_cookies_from_browser_, navigation_id_, |
| shared_storage_writable_eligible_, is_ad_tagged_, |
| force_no_https_upgrade_), |
| std::move(navigation_ui_data), service_worker_handle_.get(), |
| std::move(prefetched_signed_exchange_cache_), this, loader_type, |
| CreateCookieAccessObserver(), CreateTrustTokenAccessObserver(), |
| CreateSharedDictionaryAccessObserver(), |
| static_cast<StoragePartitionImpl*>(partition) |
| ->CreateURLLoaderNetworkObserverForNavigationRequest(*this), |
| NetworkServiceDevToolsObserver::MakeSelfOwned(frame_tree_node_), |
| CreateDeviceBoundSessionObserver(), std::move(cached_response_head), |
| std::move(interceptor)); |
| DCHECK(!HasRenderFrameHost()); |
| |
| // If needed, perform an early RenderFrameHost swap after notifying observers |
| // with DidStartNavigation, after processing WillStartRequest throttle events, |
| // and after creating the NavigationURLLoader above (which needs the old |
| // current_frame_host()). This (1) avoids performing the early swap in case |
| // the navigation gets canceled prior to getting here, and (2) ensures that |
| // DidStartNavigation and WillStartRequest implementations are not disrupted |
| // by the early swap and don't see a RenderFrameHostChanged event prior to a |
| // navigation actually starting. |
| // |
| // TODO(crbug.com/40276607): Remove the `is_called_after_did_start_navigation` |
| // param once all early swaps are done from here. |
| frame_tree_node_->render_manager()->PerformEarlyRenderFrameHostSwapIfNeeded( |
| this, /*is_called_after_did_start_navigation=*/true); |
| |
| base::UmaHistogramTimes( |
| base::StrCat({"Navigation.WillStartRequestToLoaderStart.", |
| IsInMainFrame() ? "MainFrame" : "Subframe"}), |
| base::TimeTicks::Now() - will_start_request_time_); |
| absl::Cleanup scoped_set_now = [navigation_request = |
| weak_factory_.GetWeakPtr()] { |
| if (navigation_request) { |
| navigation_request->navigation_handle_timing_.loader_start_time = |
| base::TimeTicks::Now(); |
| navigation_request->GetDelegate()->DidUpdateNavigationHandleTiming( |
| navigation_request.get()); |
| } |
| }; |
| |
| base::WeakPtr<NavigationRequest> this_ptr(weak_factory_.GetWeakPtr()); |
| loader_->Start(); |
| |
| if (!this_ptr) { |
| // `this` have been deleted by NavigationURLLoader::Start |
| // DO NOT ADD CODE HERE. |
| return; |
| } |
| |
| // Try to create the speculative RFH after sending the network request |
| // if DeferSpeculativeRFHCreation is enabled. |
| // Only create the speculative RFH if it is a normal loading rather than |
| // a BFCache restore or prerender activation. Otherwise `OnResponseStarted` |
| // will be called instantly and the creation of the speculative RFH is |
| // redundant. |
| // TODO(crbug.com/394732486): All the speculative RFH creation will be skipped |
| // if kDeferSpeculativeRFHWaitUntilFinalResponse is set. The behavior can |
| // be more adaptive by limiting to sites that commonly use COOP. |
| if (base::FeatureList::IsEnabled(features::kDeferSpeculativeRFHCreation) && |
| !kDeferSpeculativeRFHWaitUntilFinalResponse.Get() && |
| GetAssociatedRFHType() == AssociatedRenderFrameHostType::NONE) { |
| if (features::kCreateSpeculativeRFHFilterRestore.Get() && |
| loader_type != NavigationURLLoader::LoaderType::kRegular) { |
| return; |
| } |
| auto create_speculative_rfh_task = base::BindOnce( |
| [](base::WeakPtr<NavigationRequest> request, int64_t navigation_id) { |
| if (!request || request->state_ >= WILL_PROCESS_RESPONSE || |
| request->HasRenderFrameHost()) { |
| return; |
| } |
| auto rfh_creation_result = |
| request->frame_tree_node_->render_manager() |
| ->GetFrameHostForNavigation( |
| request.get(), &request->browsing_context_group_swap_, |
| ProcessAllocationContext::CreateForNavigationRequest( |
| ProcessAllocationNavigationStage:: |
| kAfterNetworkRequest, |
| navigation_id)); |
| if (rfh_creation_result.has_value()) { |
| request->SetExpectedProcessIfAssociated(); |
| } |
| }, |
| weak_factory_.GetWeakPtr(), navigation_id_); |
| int delay_ms = features::kCreateSpeculativeRFHDelayMs.Get(); |
| if (delay_ms > 0) { |
| GetUIThreadTaskRunner()->PostDelayedTask( |
| FROM_HERE, std::move(create_speculative_rfh_task), |
| base::Milliseconds(delay_ms)); |
| } else { |
| std::move(create_speculative_rfh_task).Run(); |
| } |
| } |
| } |
| |
| void NavigationRequest::OnServiceWorkerAccessed( |
| const GURL& scope, |
| AllowServiceWorkerResult allowed) { |
| GetDelegate()->OnServiceWorkerAccessed(this, scope, allowed); |
| } |
| |
| network::mojom::WebSandboxFlags NavigationRequest::SandboxFlagsInitiator() { |
| return sandbox_flags_initiator_; |
| } |
| |
| network::mojom::WebSandboxFlags NavigationRequest::SandboxFlagsInherited() { |
| return commit_params_->frame_policy.sandbox_flags; |
| } |
| |
| network::mojom::WebSandboxFlags NavigationRequest::SandboxFlagsToCommit() { |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE); |
| DCHECK(!IsSameDocument()); |
| DCHECK(!IsPageActivation()); |
| return policy_container_builder_->FinalPolicies().sandbox_flags; |
| } |
| |
| void NavigationRequest::MaybeAddResourceTimingEntryForCancelledNavigation() { |
| // Some navigation are cancelled even before requesting and receiving a |
| // response. Those cases are not supported and the ResourceTiming is not |
| // reported to the parent. |
| if (!response()) { |
| return; |
| } |
| |
| network::URLLoaderCompletionStatus status; |
| status.encoded_data_length = response()->encoded_data_length; |
| status.completion_time = base::TimeTicks::Now(); |
| AddResourceTimingEntryForFailedSubframeNavigation(status); |
| } |
| |
| void NavigationRequest::AddResourceTimingEntryForFailedSubframeNavigation( |
| const network::URLLoaderCompletionStatus& status) { |
| // For TAO-fail navigations, we would resort to fallback timing. |
| // See HTMLFrameOwnerElement::ReportFallbackResourceTimingIfNeeded(). |
| DCHECK(response()); |
| if (commit_params().navigation_timing->parent_resource_timing_access == |
| blink::mojom::ParentResourceTimingAccess::kDoNotReport) { |
| return; |
| } |
| |
| network::mojom::URLResponseHeadPtr response_head = response()->Clone(); |
| |
| bool allow_response_details = |
| commit_params().navigation_timing->parent_resource_timing_access == |
| blink::mojom::ParentResourceTimingAccess::kReportWithResponseDetails; |
| |
| GetParentFrame()->AddResourceTimingEntryForFailedSubframeNavigation( |
| frame_tree_node(), common_params().navigation_start, |
| commit_params().navigation_timing->redirect_end, |
| commit_params().original_url, common_params().url, |
| std::move(response_head), allow_response_details, status); |
| } |
| |
| void NavigationRequest::OnRedirectChecksComplete( |
| NavigationThrottle::ThrottleCheckResult result) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::OnRedirectChecksComplete", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| DCHECK(result.action() != NavigationThrottle::DEFER); |
| DCHECK(result.action() != NavigationThrottle::BLOCK_RESPONSE); |
| DCHECK(!IsPageActivation()); |
| |
| bool collapse_frame = |
| result.action() == NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE; |
| |
| // Abort the request if needed. This will destroy the NavigationRequest. |
| if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::CANCEL) { |
| // TODO(clamy): distinguish between CANCEL and CANCEL_AND_IGNORE if needed. |
| DCHECK(result.action() == NavigationThrottle::CANCEL || |
| result.net_error_code() == net::ERR_ABORTED); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(result.net_error_code()); |
| completion_status.extended_error_code = |
| static_cast<int>(ErrorNavigationTrigger::kNavigationThrottleCancel); |
| OnRequestFailedInternal(std::move(completion_status), |
| true /* skip_throttles */, |
| result.error_page_content(), collapse_frame); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (result.action() == NavigationThrottle::BLOCK_REQUEST || |
| result.action() == NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE) { |
| DCHECK(net::IsRequestBlockedError(result.net_error_code())); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(result.net_error_code()); |
| if (result.net_error_code() == net::ERR_ABORTED) { |
| completion_status.extended_error_code = |
| static_cast<int>(ErrorNavigationTrigger::kNavigationThrottleBlock); |
| } |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(result.net_error_code()), |
| true /* skip_throttles */, result.error_page_content(), collapse_frame); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| devtools_instrumentation::OnNavigationRequestWillBeSent(*this); |
| |
| net::HttpRequestHeaders modified_headers = TakeModifiedRequestHeaders(); |
| std::vector<std::string> removed_headers = TakeRemovedRequestHeaders(); |
| |
| // The topics a request is allowed to see can change within its redirect |
| // chain thus we need to recalculate them. For example, different caller |
| // origins (i.e. navigation URL's origin) may receive different topics, as the |
| // callers can only get the topics about the sites they were on. Besides, |
| // regardless of cross-origin-ness, the timestamp can also affect the |
| // candidate epochs where the topics are derived from, thus resulting in |
| // different topics across redirects. |
| if (topics_eligible_) { |
| topics_eligible_ = false; |
| |
| // At this point we may not have a valid `GetRenderFrameHost()` if the |
| // navigation is during a cross-site redirect. Thus, pass in the current/old |
| // RenderFrameHost here. This is fine, because it should still give us the |
| // desired IsPrimary() status and the desired top-level frame that |
| // `HandleTopicsEligibleResponse()` is interested in knowing. |
| HandleTopicsEligibleResponse( |
| commit_params_->redirect_response.back().get()->parsed_headers, |
| url::Origin::Create(commit_params_->redirects.back()), |
| *frame_tree_node_->current_frame_host(), |
| browsing_topics::ApiCallerSource::kIframeAttribute); |
| } |
| |
| // Removes the topics header. This will effectively be a no-op if the topics |
| // header wasn't sent for the previous request. |
| removed_headers.push_back(kBrowsingTopicsRequestHeaderKey); |
| |
| TopicsHeaderValueResult topics_header_value_result = |
| GetTopicsHeaderValueForNavigationRequest(frame_tree_node_, |
| common_params_->url); |
| |
| topics_eligible_ = topics_header_value_result.topics_eligible; |
| |
| if (topics_header_value_result.header_value) { |
| modified_headers.SetHeader(kBrowsingTopicsRequestHeaderKey, |
| *topics_header_value_result.header_value); |
| } |
| |
| if (ad_auction_headers_eligible_) { |
| // Redirects are ineligible for ad auction headers. |
| ad_auction_headers_eligible_ = false; |
| removed_headers.push_back(kAdAuctionRequestHeaderKey); |
| } |
| |
| if (shared_storage_writable_opted_in_) { |
| // On a redirect, the PermissionsPolicy may change the status of this |
| // request's Shared Storage eligibility, so we need to re-compute it. |
| bool previous_shared_storage_writable_eligible = |
| shared_storage_writable_eligible_; |
| shared_storage_writable_eligible_ = |
| IsSharedStorageWritableEligibleForNavigationRequest( |
| frame_tree_node_, common_params_->url); |
| |
| if (shared_storage_writable_eligible_ != |
| previous_shared_storage_writable_eligible) { |
| if (shared_storage_writable_eligible_) { |
| modified_headers.SetHeader(kSecSharedStorageWritableRequestHeaderKey, |
| "?1"); |
| } else { |
| removed_headers.push_back(kSecSharedStorageWritableRequestHeaderKey); |
| } |
| } |
| } |
| |
| // Removes all Client Hints from the request, that were passed on from the |
| // previous one. |
| for (const auto& elem : network::GetClientHintToNameMap()) { |
| const auto& header = elem.second; |
| removed_headers.push_back(header); |
| } |
| |
| // Add any required Client Hints to the current request. |
| BrowserContext* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| ClientHintsControllerDelegate* client_hints_delegate = |
| browser_context->GetClientHintsControllerDelegate(); |
| if (client_hints_delegate) { |
| net::HttpRequestHeaders client_hints_extra_headers; |
| const url::Origin& source_origin = |
| url::Origin::Create(commit_params_->redirects.back()); |
| const network::mojom::URLResponseHead* response_head = |
| commit_params_->redirect_response.back().get(); |
| ParseAndPersistAcceptCHForNavigation( |
| source_origin, response_head->parsed_headers, |
| response_head->headers.get(), browser_context, client_hints_delegate, |
| frame_tree_node_); |
| |
| AddNavigationRequestClientHintsHeaders( |
| GetTentativeOriginAtRequestTime(), &client_hints_extra_headers, |
| browser_context, client_hints_delegate, is_overriding_user_agent(), |
| frame_tree_node_, commit_params_->frame_policy.container_policy, |
| common_params_->url); |
| modified_headers.MergeFrom(client_hints_extra_headers); |
| // On a redirect, unless devtools has overridden the User-Agent header, we |
| // should send the User-Agent string based on policy. |
| if (!devtools_user_agent_override_) { |
| modified_headers.SetHeader( |
| net::HttpRequestHeaders::kUserAgent, |
| ComputeUserAgentValue(modified_headers, GetUserAgentOverride(), |
| browser_context)); |
| } |
| } |
| |
| // Add reduced accept language header to the current request. |
| // If devtools has overridden the Accept-Language header, skip reduce |
| // Accept-Language header. |
| if (auto reduce_accept_lang_utils = |
| ReduceAcceptLanguageUtils::Create(browser_context); |
| reduce_accept_lang_utils && !devtools_accept_language_override_) { |
| if (!ReduceAcceptLanguageUtils::CheckDisableReduceAcceptLanguageOriginTrial( |
| common_params_->url, frame_tree_node_, |
| browser_context->GetOriginTrialsControllerDelegate())) { |
| net::HttpRequestHeaders accept_language_headers; |
| std::optional<std::string> reduced_accept_language = |
| reduce_accept_lang_utils.value() |
| .AddNavigationRequestAcceptLanguageHeaders( |
| url::Origin::Create(common_params_->url), frame_tree_node_, |
| &accept_language_headers); |
| commit_params_->reduced_accept_language = |
| reduced_accept_language.value_or(""); |
| modified_headers.MergeFrom(accept_language_headers); |
| } else { |
| // Remove the Accept-Language header passed from previous request, if any. |
| removed_headers.push_back(net::HttpRequestHeaders::kAcceptLanguage); |
| commit_params_->reduced_accept_language = ""; |
| } |
| } |
| |
| net::HttpRequestHeaders cors_exempt_headers; |
| std::swap(cors_exempt_headers, cors_exempt_request_headers_); |
| loader_->FollowRedirect(std::move(removed_headers), |
| std::move(modified_headers), |
| std::move(cors_exempt_headers)); |
| } |
| |
| void NavigationRequest::OnFailureChecksComplete( |
| NavigationThrottle::ThrottleCheckResult result) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::OnFailureChecksComplete", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| // This method is called as a result of getting to the end of |
| // OnRequestFailedInternal(), which calls WillFailRequest(), which |
| // runs the throttles, which eventually call back to this method. |
| DCHECK(result.action() != NavigationThrottle::DEFER); |
| |
| // The throttle may have changed the net_error_code, so we set the |
| // `net_error_` again, overriding what OnRequestFailedInternal() set. |
| net::Error old_net_error = net_error_; |
| ErrorPageProcess old_error_page_process = ComputeErrorPageProcess(); |
| net_error_ = result.net_error_code(); |
| |
| // The new `net_error_` value may mean we want to cancel the navigation. |
| if (MaybeCancelFailedNavigation()) { |
| return; |
| } |
| |
| // FIXME: Should we clear out |extended_error_code_| here? |
| |
| // Ensure that WillFailRequest() isn't changing the error code in a way that |
| // switches the destination process for the error page - see |
| // https://crbug.com/817881. |
| // NOTE: The throttle may change the error code to cancel a navigation which |
| // could lead to a failure here, and therefore this must be done after |
| // `MaybeCancelFailedNavigation`. |
| CHECK_EQ(old_error_page_process, ComputeErrorPageProcess()) |
| << " Unsupported error code change in WillFailRequest(): from " |
| << old_net_error << " to " << net_error_; |
| |
| // The OnRequestFailedInternal() did not commit the error page as it |
| // deferred to WillFailRequest(), which has called through to here, and |
| // now we are finally ready to commit the error page. This will be committed |
| // to the RenderFrameHost previously chosen in OnRequestFailedInternal(). |
| CommitErrorPage(result.error_page_content()); |
| // DO NOT ADD CODE after this. The previous call to CommitErrorPage() |
| // caused the destruction of the NavigationRequest. |
| } |
| |
| void NavigationRequest::OnWillProcessResponseChecksComplete( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK(result.action() != NavigationThrottle::DEFER); |
| |
| // If the NavigationThrottles allowed the navigation to continue, have the |
| // processing of the response resume in the network stack. |
| if (result.action() == NavigationThrottle::PROCEED) { |
| // If this is a download and NavigationThrottles allowed it, intercept the |
| // navigation response, pass it to DownloadManager, and cancel the |
| // navigation. |
| if (is_download_) { |
| // TODO(arthursonzogni): Pass the real ResourceRequest. For the moment |
| // only these parameters will be used, but it may evolve quickly. |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = common_params_->url; |
| resource_request->method = common_params_->method; |
| resource_request->request_initiator = common_params_->initiator_origin; |
| resource_request->referrer = common_params_->referrer->url; |
| resource_request->has_user_gesture = common_params_->has_user_gesture; |
| resource_request->mode = network::mojom::RequestMode::kNavigate; |
| resource_request->transition_type = common_params_->transition; |
| resource_request->trusted_params = |
| network::ResourceRequest::TrustedParams(); |
| resource_request->trusted_params->isolation_info = GetIsolationInfo(); |
| |
| BrowserContext* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| DownloadManagerImpl* download_manager = static_cast<DownloadManagerImpl*>( |
| browser_context->GetDownloadManager()); |
| if (!response_head_->client_side_content_decoding_types.empty()) { |
| CHECK(base::FeatureList::IsEnabled( |
| network::features::kRendererSideContentDecoding)); |
| // If content decoding is required, perform the decoding in the network |
| // service. |
| |
| // Attempt to create the data pipe needed for content decoding. |
| auto data_pipe_pair = |
| network::ContentDecodingInterceptor::CreateDataPipePair( |
| network::ContentDecodingInterceptor::ClientType::kDownload); |
| if (!data_pipe_pair) { |
| // Handle data pipe creation failure. This is rare but can happen if |
| // shared memory is exhausted. In such a situation, the page load will |
| // likely fail anyway as resources cannot be properly loaded. However, |
| // we should avoid crashing the browser process or attempting to |
| // download the raw encoded body. Instead, just abort the navigation |
| // request. |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kContentDecoderDataPipeCreationFailed); |
| OnRequestFailedInternal(completion_status, |
| /*skip_throttles=*/false, |
| /*error_page_content=*/std::nullopt, |
| /*collapse_frame=*/false); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| network::ContentDecodingInterceptor::InterceptOnNetworkService( |
| *GetNetworkService(), |
| response_head_->client_side_content_decoding_types, |
| url_loader_client_endpoints_, response_body_, |
| std::move(*data_pipe_pair)); |
| } |
| download_manager->InterceptNavigation( |
| std::move(resource_request), redirect_chain_, response_head_.Clone(), |
| std::move(response_body_), std::move(url_loader_client_endpoints_), |
| ssl_info_.has_value() ? ssl_info_->cert_status : 0, |
| frame_tree_node_->frame_tree_node_id(), |
| from_download_cross_origin_redirect_); |
| |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kShouldNotRenderResponse); |
| OnRequestFailedInternal(completion_status, false /*skip_throttles*/, |
| std::nullopt /*error_page_content*/, |
| false /*collapse_frame*/); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailed has |
| // destroyed the NavigationRequest. |
| return; |
| } |
| |
| // Per https://whatwg.org/C/iframe-embed-object.html#the-object-element, |
| // this implements step 4.7 from "determine what the object element |
| // represents": "If the load failed (e.g. there was an HTTP 404 error, there |
| // was a DNS error), fire an event named error at the element, then jump to |
| // the step below labeled fallback." |
| // |
| // This case handles HTTP errors, which are otherwise considered a |
| // "successful" navigation. |
| // |
| // TODO(dcheng): According to the standard, an <object> element shouldn't |
| // even have a browsing context associated with it unless a successful |
| // navigation would commit in it. |
| if (response()->headers && |
| ShouldRenderFallbackContentForResponse(*response()->headers)) { |
| // Spin up a helper to drain the response body for this navigation. The |
| // helper will request fallback content (triggering completion) and report |
| // the resource timing info once the entire response body is drained. |
| // |
| // The response body fetcher takes advantage of base::SupportsUserData to |
| // ensure that the fetcher does not outlive `this`. This ensures that the |
| // fallback / resource timing are only reported if the navigation request |
| // is logically still pending. |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kShouldRenderFallbackContent); |
| ObjectNavigationFallbackBodyLoader::CreateAndStart( |
| *this, std::move(response_body_), |
| std::move(url_loader_client_endpoints_), |
| base::BindOnce(&NavigationRequest::OnRequestFailedInternal, |
| weak_factory_.GetWeakPtr(), completion_status, |
| false /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */)); |
| // Unlike the other early returns, intentionally skip calling |
| // `OnRequestFailedInternal()`. This allows the response body drainer to |
| // track the lifetime of `this` and skip the remaining work if `this` is |
| // deleted before the response body is completely loaded. |
| return; |
| } |
| } |
| |
| // Abort the request if needed. This includes requests that were blocked by |
| // NavigationThrottles and requests that should not commit (e.g. downloads, |
| // 204/205s). This will destroy the NavigationRequest. |
| if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::CANCEL || |
| !response_should_be_rendered_) { |
| MaybeAddResourceTimingEntryForCancelledNavigation(); |
| |
| // TODO(clamy): distinguish between CANCEL and CANCEL_AND_IGNORE. |
| if (!response_should_be_rendered_) { |
| auto completion_status = |
| network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| completion_status.extended_error_code = static_cast<int32_t>( |
| ErrorNavigationTrigger::kShouldNotRenderResponse); |
| OnRequestFailedInternal(completion_status, true /* skip_throttles */, |
| std::nullopt /* error_page_content */, |
| false /* collapse_frame */); |
| |
| // DO NOT ADD CODE after this. The previous call to |
| // OnRequestFailedInternal has destroyed the NavigationRequest. |
| return; |
| } |
| |
| DCHECK(result.action() == NavigationThrottle::CANCEL || |
| result.net_error_code() == net::ERR_ABORTED); |
| auto completion_status = |
| network::URLLoaderCompletionStatus(result.net_error_code()); |
| completion_status.extended_error_code = |
| static_cast<int32_t>(ErrorNavigationTrigger::kNavigationThrottleCancel); |
| OnRequestFailedInternal(completion_status, true /* skip_throttles */, |
| result.error_page_content(), |
| false /* collapse_frame */); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| if (result.action() == NavigationThrottle::BLOCK_RESPONSE) { |
| DCHECK_EQ(net::ERR_BLOCKED_BY_RESPONSE, result.net_error_code()); |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(result.net_error_code()), |
| true /* skip_throttles */, result.error_page_content(), |
| false /* collapse_frame */); |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| DCHECK_EQ(result.action(), NavigationThrottle::PROCEED); |
| |
| // When this request is for prerender activation, `commit_deferrer_` has |
| // already been processed. |
| if (IsPrerenderedPageActivation()) { |
| DCHECK(!commit_deferrer_); |
| CommitNavigation(); |
| // DO NOT ADD CODE after this. The previous call to CommitNavigation |
| // destroyed the NavigationRequest. |
| return; |
| } |
| |
| RunCommitDeferringConditions(); |
| // DO NOT ADD CODE after this. The previous call to |
| // RunCommitDeferringConditions may have caused the destruction of the |
| // NavigationRequest. |
| } |
| |
| void NavigationRequest::OnWillCommitWithoutUrlLoaderChecksComplete( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK(result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::PROCEED); |
| if (result.action() == NavigationThrottle::CANCEL_AND_IGNORE) { |
| OnRequestFailedInternal( |
| network::URLLoaderCompletionStatus(result.net_error_code()), |
| /*skip_throttles=*/true, result.error_page_content(), |
| /*collapse_frame=*/false); |
| |
| // DO NOT ADD CODE after this. The previous call to OnRequestFailedInternal |
| // has destroyed the NavigationRequest. |
| return; |
| } |
| |
| // Ensure that bfcache and other non-UrlLoader history navigations can |
| // dispatch the Navigation API's navigate event as the navigation is starting. |
| // Cases with a UrlLoader are handled in OnStartChecksComplete. |
| MaybeDispatchNavigateEventForCrossDocumentTraversal(); |
| |
| InheritServiceWorkerControllerFromParentIfNeeded(); |
| |
| CommitNavigation(); |
| } |
| |
| void NavigationRequest::InheritServiceWorkerControllerFromParentIfNeeded() { |
| CHECK(!loader_); |
| CHECK(!service_worker_handle_); |
| RenderFrameHostImpl* parent = frame_tree_node()->parent(); |
| if (!parent || !GetURL().IsAboutSrcdoc()) { |
| return; |
| } |
| base::WeakPtr<ServiceWorkerClient> parent_service_worker_client = |
| parent->GetLastCommittedServiceWorkerClient(); |
| if (!parent_service_worker_client) { |
| return; |
| } |
| |
| if ((frame_tree_node_->pending_frame_policy().sandbox_flags & |
| network::mojom::WebSandboxFlags::kOrigin) == |
| network::mojom::WebSandboxFlags::kOrigin) { |
| return; |
| } |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| frame_tree_node_->current_frame_host(), |
| blink::mojom::WebFeature::kAboutSrcdocToBeControlledByServiceWorker); |
| if (!base::FeatureList::IsEnabled(features::kServiceWorkerSrcdocSupport) || |
| !GetContentClient()->browser()->AllowServiceWorkerToControlSrcdocIframe( |
| frame_tree_node_->navigator().controller().GetBrowserContext())) { |
| return; |
| } |
| |
| StoragePartition* partition = GetStoragePartitionWithCurrentSiteInfo(); |
| auto* service_worker_context = static_cast<ServiceWorkerContextWrapper*>( |
| partition->GetServiceWorkerContext()); |
| // As ServiceWorkerMainResourceHandle is not used for intercepting the srcdoc |
| // iframe main resource, the fetch event client id is not used. Use empty |
| // string as fetch_event_client_id when creating it. |
| service_worker_handle_ = std::make_unique<ServiceWorkerMainResourceHandle>( |
| service_worker_context, base::DoNothing(), |
| /*fetch_event_client_id=*/std::string(), parent_service_worker_client); |
| service_worker_handle_->set_service_worker_client( |
| service_worker_context->context() |
| ->service_worker_client_owner() |
| .CreateServiceWorkerClientForWindow( |
| frame_tree_node_->AreAncestorsSecure(), |
| frame_tree_node_->frame_tree_node_id()), |
| GetIsolationInfo()); |
| service_worker_handle_->service_worker_client()->InheritControllerFrom( |
| *parent_service_worker_client, net::SimplifyUrlForRequest(GetURL())); |
| } |
| |
| void NavigationRequest::RunCommitDeferringConditions() { |
| commit_deferrer_->RegisterDeferringConditions(*this); |
| commit_deferrer_->ProcessChecks(); |
| // DO NOT ADD CODE after this. The previous call to ProcessChecks may have |
| // caused the destruction of the NavigationRequest. |
| } |
| |
| void NavigationRequest::OnCommitDeferringConditionChecksComplete( |
| CommitDeferringCondition::NavigationType navigation_type, |
| std::optional<FrameTreeNodeId> candidate_prerender_frame_tree_node_id) { |
| switch (navigation_type) { |
| case CommitDeferringCondition::NavigationType::kPrerenderedPageActivation: |
| OnPrerenderingActivationChecksComplete( |
| navigation_type, candidate_prerender_frame_tree_node_id); |
| // DO NOT ADD CODE after this. The previous call to |
| // OnPrerenderingActivationChecksComplete caused the destruction of the |
| // NavigationRequest. |
| return; |
| case CommitDeferringCondition::NavigationType::kOther: |
| DCHECK_LT(state_, READY_TO_COMMIT); |
| CommitNavigation(); |
| // DO NOT ADD CODE after this. The previous call to CommitNavigation |
| // caused the destruction of the NavigationRequest. |
| return; |
| } |
| } |
| |
| void NavigationRequest::CommitErrorPage( |
| const std::optional<std::string>& error_page_content) { |
| DCHECK(!IsSameDocument()); |
| |
| DetermineOriginAgentClusterEndResult(); |
| |
| UpdateHistoryParamsInCommitNavigationParams(); |
| |
| common_params_->should_replace_current_entry = |
| ShouldReplaceCurrentEntryForFailedNavigation(); |
| |
| // Don't pass the base url in a failed navigation. |
| common_params_->initiator_base_url = std::nullopt; |
| |
| if (request_navigation_client_.is_bound()) { |
| if (GetRenderFrameHost() == |
| RenderFrameHostImpl::FromID( |
| current_render_frame_host_id_at_construction_)) { |
| // Reuse the request NavigationClient for commit. |
| commit_navigation_client_ = std::move(request_navigation_client_); |
| } else { |
| // This navigation is cross-RenderFrameHost: the original document should |
| // no longer be able to cancel it. |
| IgnoreInterfaceDisconnection(); |
| } |
| } |
| |
| topics_eligible_ = false; |
| |
| ad_auction_headers_eligible_ = false; |
| |
| base::WeakPtr<NavigationRequest> weak_self(weak_factory_.GetWeakPtr()); |
| ReadyToCommitNavigation(true /* is_error */); |
| // The caller above might result in the deletion of `this`. Return immediately |
| // if so. |
| if (!weak_self) { |
| return; |
| } |
| |
| // Ensure the renderer does not reuse the document sequence number for |
| // cross-origin navigations (which can lead to later bugs with same-document |
| // navigations appearing to be cross-origin). All error pages have unique |
| // opaque origins and are considered cross-origin. |
| // |
| // The one exception is for transitioning to or from a compatible error page, |
| // which preserves state in case a temporary failure later succeeds. Here, we |
| // must check for any cases where the committing error page has a valid |
| // precursor that agrees with the current document, whether the current |
| // document is already an error page or not. (The renderer process will narrow |
| // DSN reuse further, to cases the URL is a closer match, ignoring fragments.) |
| // See also CommitNavigation for the other direction. |
| // TODO(crbug.com/396645697): Use a different technique for preserving history |
| // item state on error pages, separate from the error page's own state. |
| RenderFrameHostImpl* previous_rfh = frame_tree_node()->current_frame_host(); |
| const url::Origin& previous_origin = previous_rfh->GetLastCommittedOrigin(); |
| bool is_error_page_with_same_precursor = |
| previous_origin.GetTupleOrPrecursorTupleIfOpaque().IsValid() && |
| commit_params_->origin_to_commit.GetTupleOrPrecursorTupleIfOpaque() == |
| previous_origin.GetTupleOrPrecursorTupleIfOpaque(); |
| if (!is_error_page_with_same_precursor) { |
| commit_params_->force_new_document_sequence_number = true; |
| } |
| |
| PopulateDocumentTokenForCrossDocumentNavigation(); |
| // Use a separate cache shard, and no cookies, for error pages. |
| isolation_info_for_subresources_ = |
| net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt); |
| GetRenderFrameHost()->FailedNavigation( |
| this, *common_params_, *commit_params_, has_stale_copy_in_cache_, |
| net_error_, extended_error_code_, error_page_content, *document_token_); |
| UpdateNavigationHandleTimingsOnCommitSent(); |
| |
| SendDeferredConsoleMessages(); |
| } |
| |
| void NavigationRequest::AddOldPageInfoToCommitParamsIfNeeded() { |
| // Add the routing ID and the updated lifecycle state of the old page if we |
| // need to run pagehide and visibilitychange handlers of the old page |
| // when we commit the new page. |
| auto* old_frame_host = |
| frame_tree_node_->render_manager()->current_frame_host(); |
| if (!GetRenderFrameHost() |
| ->ShouldDispatchPagehideAndVisibilitychangeDuringCommit( |
| old_frame_host, GetUrlInfo())) { |
| return; |
| } |
| DCHECK(!IsSameDocument()); |
| // The pagehide event's "persisted" property depends on whether the old page |
| // will be put into the back-forward cache or not. As we won't freeze the |
| // page until after the commit finished, there is no way to know for sure |
| // whether the page will actually be persisted or not after commit, but we |
| // will send our best guess by checking if the page can be persisted at this |
| // point. |
| bool can_store_old_page_in_bfcache = |
| frame_tree_node_->frame_tree() |
| .controller() |
| .GetBackForwardCache() |
| .GetFutureBackForwardCacheEligibilityPotential(old_frame_host) |
| .CanStore(); |
| commit_params_->old_page_info = blink::mojom::OldPageInfo::New(); |
| commit_params_->old_page_info->frame_token_for_old_main_frame = |
| old_frame_host->GetFrameToken(); |
| auto* page_lifecycle_state_manager = |
| old_frame_host->render_view_host()->GetPageLifecycleStateManager(); |
| commit_params_->old_page_info->new_lifecycle_state_for_old_page = |
| page_lifecycle_state_manager->SetPagehideDispatchDuringNewPageCommit( |
| can_store_old_page_in_bfcache /* persisted */); |
| } |
| |
| bool NavigationRequest::ShouldDispatchPageSwapEvent() const { |
| if (early_render_frame_host_swap_type_ != |
| EarlyRenderFrameHostSwapType::kNone) { |
| return false; |
| } |
| |
| if (IsSameDocument()) { |
| return false; |
| } |
| |
| return !did_fire_page_swap_; |
| } |
| |
| void NavigationRequest::CommitNavigation() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::CommitNavigation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| // A navigation request should only commit once the response has been |
| // processed. |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE); |
| // If a WebUI was created for this navigation, it must have been moved to the |
| // RenderFrameHost we're about to commit in already. |
| CHECK(!HasWebUI()); |
| CheckSoftNavigationHeuristicsInvariants(); |
| |
| CoopCoepSanityCheck(); |
| |
| DetermineOriginAgentClusterEndResult(); |
| |
| UpdateHistoryParamsInCommitNavigationParams(); |
| DCHECK(NeedsUrlLoader() == !!response_head_ || |
| (was_redirected_ && common_params_->url.IsAboutBlank())); |
| DCHECK(!common_params_->url.SchemeIs(url::kJavaScriptScheme)); |
| DCHECK(!blink::IsRendererDebugURL(common_params_->url)); |
| |
| AddOldPageInfoToCommitParamsIfNeeded(); |
| if (ShouldDispatchPageSwapEvent()) { |
| frame_tree_node_->current_frame_host() |
| ->GetAssociatedLocalFrame() |
| ->DispatchPageSwap(WillDispatchPageSwap()); |
| } |
| |
| url::Origin origin_to_commit = GetOriginToCommit().value(); |
| if (base::FeatureList::IsEnabled(features::kValidateCommitOriginAtCommit)) { |
| ValidateCommitOrigin(origin_to_commit); |
| } |
| isolation_info_for_subresources_ = |
| GetRenderFrameHost()->ComputeIsolationInfoForSubresourcesForPendingCommit( |
| origin_to_commit, is_credentialless(), ComputeFencedFrameNonce()); |
| DCHECK(!isolation_info_for_subresources_.IsEmpty()); |
| |
| // If this is a srcdoc document, the content comes from the parent frame, so |
| // the origin must be the parent and not the initiator. In this case, do not |
| // inherit the base URI from the initiator if the origins do not agree |
| // (accounting for the case that the chosen origin might be opaque with a |
| // precursor of the parent's origin, in a sandboxed case). There should also |
| // not be an initiator base URL if there is no initiator origin, such as in a |
| // browser-initiated navigation. |
| if (GetURL().IsAboutSrcdoc() && |
| (!common_params().initiator_origin || |
| origin_to_commit.GetTupleOrPrecursorTupleIfOpaque() != |
| common_params() |
| .initiator_origin->GetTupleOrPrecursorTupleIfOpaque())) { |
| // TODO(crbug.com/40165505): Make this unreachable by blocking |
| // cross-origin about:srcdoc navigations. Then enforce that the chosen |
| // origin for srcdoc cases agrees with the parent frame's origin. |
| common_params_->initiator_base_url = std::nullopt; |
| } |
| |
| // TODO(crbug.com/40092527): The storage key's origin is ignored at the |
| // moment. We will be able to use it once the browser can compute the origin |
| // to commit. |
| std::optional<base::UnguessableToken> nonce = |
| GetRenderFrameHost()->ComputeNonce(is_credentialless(), |
| ComputeFencedFrameNonce()); |
| |
| commit_params_->storage_key = GetRenderFrameHost()->CalculateStorageKey( |
| origin_to_commit, base::OptionalToPtr(nonce)); |
| |
| if (topics_eligible_) { |
| topics_eligible_ = false; |
| |
| if (response()) { |
| HandleTopicsEligibleResponse( |
| response_head_->parsed_headers, url::Origin::Create(GetURL()), |
| *GetRenderFrameHost(), |
| browsing_topics::ApiCallerSource::kIframeAttribute); |
| } |
| } |
| |
| if (ad_auction_headers_eligible_) { |
| ProcessAdAuctionResponseHeaders(origin_to_commit, *GetRenderFrameHost(), |
| response() ? response()->headers : nullptr); |
| } else if (has_ad_auction_headers_attribute_) { |
| RemoveAdAuctionResponseHeaders(response() ? response()->headers : nullptr); |
| } |
| |
| RenderFrameHostImpl* old_frame_host = |
| frame_tree_node_->render_manager()->current_frame_host(); |
| if (!NavigationTypeUtils::IsSameDocument(common_params_->navigation_type)) { |
| // We want to record this for the frame that we are navigating away from. |
| old_frame_host->RecordNavigationSuddenTerminationHandlers(); |
| } |
| if (IsServedFromBackForwardCache() || IsPrerenderedPageActivation()) { |
| CommitPageActivation(); |
| return; |
| } |
| |
| // For consistency, prerendering activation *should* go through this as well. |
| // However, the prerender implementation based on swapping WebContents |
| // introduces a number of edge cases that navigation code wouldn't normally |
| // have to handle, so it's easier to simply skip this in the hopes that the |
| // non-mparch implementation will be removed soon. |
| base::WeakPtr<NavigationRequest> weak_self(weak_factory_.GetWeakPtr()); |
| ReadyToCommitNavigation(false /* is_error */); |
| // The call above might block on showing a user dialog. The interaction of |
| // the user with this dialog might result in the WebContents owning this |
| // NavigationRequest to be destroyed. Return if this is the case. |
| if (!weak_self) |
| return; |
| |
| DCHECK(GetRenderFrameHost() == old_frame_host || |
| GetRenderFrameHost() == |
| frame_tree_node_->render_manager()->speculative_frame_host()); |
| |
| if (request_navigation_client_.is_bound()) { |
| if (GetRenderFrameHost() == |
| RenderFrameHostImpl::FromID( |
| current_render_frame_host_id_at_construction_)) { |
| // Reuse the request NavigationClient for commit. |
| commit_navigation_client_ = std::move(request_navigation_client_); |
| } else { |
| // This navigation is cross-RenderFrameHost: the original document should |
| // no longer be able to cancel it. |
| IgnoreInterfaceDisconnection(); |
| } |
| } |
| |
| CreateCoepReporter(GetRenderFrameHost()->GetProcess()->GetStoragePartition()); |
| coop_status_.UpdateReporterStoragePartition( |
| GetRenderFrameHost()->GetProcess()->GetStoragePartition()); |
| CreateDipReporter(GetRenderFrameHost()->GetProcess()->GetStoragePartition()); |
| |
| BrowserContext* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| ClientHintsControllerDelegate* client_hints_delegate = |
| browser_context->GetClientHintsControllerDelegate(); |
| if (client_hints_delegate) { |
| std::optional<std::vector<network::mojom::WebClientHintsType>> |
| opt_in_hints_from_response; |
| if (response()) { |
| opt_in_hints_from_response = ParseAndPersistAcceptCHForNavigation( |
| url::Origin::Create(common_params_->url), response()->parsed_headers, |
| response()->headers.get(), browser_context, client_hints_delegate, |
| frame_tree_node_); |
| } |
| commit_params_->enabled_client_hints = |
| LookupAcceptCHForCommit(origin_to_commit, client_hints_delegate, |
| frame_tree_node_, common_params_->url); |
| } |
| // Navigation requests should use the new origin as the partition origin |
| // except if embedded in an outer frame. |
| url::Origin partition_origin = origin_to_commit; |
| bool is_top_level = frame_tree_node()->GetParentOrOuterDocument() == nullptr; |
| if (!is_top_level) { |
| partition_origin = frame_tree_node() |
| ->GetParentOrOuterDocument() |
| ->GetOutermostMainFrame() |
| ->GetLastCommittedOrigin(); |
| } |
| |
| PersistOriginTrialsFromHeaders(origin_to_commit, partition_origin, response(), |
| browser_context, GetNextPageUkmSourceId()); |
| |
| // Clean the reduced accept-language to commit if the final response have a |
| // valid deprecation origin trial token. |
| if (auto reduce_accept_lang_utils = |
| ReduceAcceptLanguageUtils::Create(browser_context); |
| reduce_accept_lang_utils && !devtools_accept_language_override_ && |
| ReduceAcceptLanguageUtils::CheckDisableReduceAcceptLanguageOriginTrial( |
| common_params_->url, frame_tree_node_, |
| browser_context->GetOriginTrialsControllerDelegate()) && |
| !commit_params_->reduced_accept_language.empty()) { |
| reduce_accept_lang_utils.value().RemoveReducedAcceptLanguage( |
| origin_to_commit, frame_tree_node_); |
| commit_params_->reduced_accept_language = ""; |
| } |
| |
| // Sticky user activation should only be preserved for same-site subframe |
| // navigations. This is done to prevent newly navigated documents from |
| // re-using the sticky user activation state from the previously navigated |
| // document in the frame. We persist user activation across same-site |
| // navigations for compatibility reasons, and this does not need to match the |
| // same-site checks used in the process model. See: crbug.com/736415. |
| // TODO(crbug.com/40228985): Remove this once we find a way to reset |
| // activation unconditionally without breaking sites in practice. |
| commit_params_->should_have_sticky_user_activation = |
| !frame_tree_node_->IsMainFrame() && |
| old_frame_host->HasStickyUserActivation() && |
| net::SchemefulSite::IsSameSite(old_frame_host->GetLastCommittedOrigin(), |
| origin_to_commit); |
| |
| // Generate a UKM source and track it on NavigationRequest. This will be |
| // passed down to the blink::Document to be created, if any, and used for UKM |
| // source creation when navigation has successfully committed. |
| commit_params_->document_ukm_source_id = ukm::UkmRecorder::GetNewSourceID(); |
| |
| blink::mojom::ServiceWorkerContainerInfoForClientPtr |
| service_worker_container_info; |
| blink::mojom::ControllerServiceWorkerInfoPtr controller; |
| |
| // Notify the service worker navigation handle that navigation commit is |
| // about to go. |
| if (service_worker_handle_ && |
| service_worker_handle_->service_worker_client()) { |
| DCHECK(coep_reporter()); |
| DCHECK(dip_reporter()); |
| mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter> |
| coep_reporter_remote; |
| coep_reporter()->Clone( |
| coep_reporter_remote.InitWithNewPipeAndPassReceiver()); |
| |
| mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter> |
| dip_reporter_remote; |
| dip_reporter()->Clone(dip_reporter_remote.InitWithNewPipeAndPassReceiver()); |
| |
| std::tie(service_worker_container_info, controller) = |
| service_worker_handle_->scoped_service_worker_client() |
| ->CommitResponseAndRelease( |
| GetRenderFrameHost()->GetGlobalId(), |
| policy_container_builder_->FinalPolicies(), |
| std::move(coep_reporter_remote), std::move(dip_reporter_remote), |
| commit_params_->document_ukm_source_id); |
| } |
| |
| // Determine if top-level navigation is allowed without sticky user |
| // activation. This is used to fix the exploit in https://crbug.com/1251790. |
| // If a child document is cross-origin with its parent, it loses its ability |
| // to navigate top without user gesture. One notable exception is made if its |
| // parent embeds it using sandbox="allow-top-navigation". Please note this is |
| // quite unusual, because it means using sandbox brings new capabilities, as |
| // opposed to new restrictions. |
| using WebSandboxFlags = network::mojom::WebSandboxFlags; |
| const bool embedder_allows_top_navigation_explicitly = |
| ((commit_params_->frame_policy.sandbox_flags != WebSandboxFlags::kNone) && |
| (commit_params_->frame_policy.sandbox_flags & |
| WebSandboxFlags::kTopNavigation) == WebSandboxFlags::kNone); |
| const bool is_same_origin_to_top = |
| origin_to_commit == |
| GetRenderFrameHost()->GetMainFrame()->GetLastCommittedOrigin(); |
| if (is_same_origin_to_top) { |
| policy_container_builder_->SetAllowTopNavigationWithoutUserGesture(true); |
| } else if (!IsInMainFrame() && !embedder_allows_top_navigation_explicitly) { |
| policy_container_builder_->SetAllowTopNavigationWithoutUserGesture(false); |
| } |
| |
| if (!IsSameDocument()) { |
| commit_params_->navigation_api_history_entry_arrays = |
| GetNavigationController()->GetNavigationApiHistoryEntryVectors( |
| frame_tree_node_, this); |
| PopulateDocumentTokenForCrossDocumentNavigation(); |
| |
| // Ensure the renderer does not reuse the document sequence number for |
| // cross-origin navigations (which can lead to later bugs with same-document |
| // navigations appearing to be cross-origin). |
| // |
| // The one exception is for transitioning to or from a compatible error |
| // page, which preserves state in case a temporary failure later succeeds. |
| // Here, we must check for the case that the user navigates from an error |
| // page to a non-error page that matches the (valid) precursor origin. See |
| // also CommitErrorPage for the other direction. |
| // TODO(crbug.com/396645697): Use a different technique for preserving |
| // history item state on error pages, separate from the error page's own |
| // state. |
| RenderFrameHostImpl* previous_rfh = frame_tree_node()->current_frame_host(); |
| const url::Origin& previous_origin = previous_rfh->GetLastCommittedOrigin(); |
| bool is_cross_origin_navigation = |
| !commit_params_->origin_to_commit.IsSameOriginWith(previous_origin); |
| bool compatible_with_error_page = |
| previous_rfh->IsErrorDocument() && |
| previous_origin.GetTupleOrPrecursorTupleIfOpaque().IsValid() && |
| commit_params_->origin_to_commit.GetTupleOrPrecursorTupleIfOpaque() == |
| previous_origin.GetTupleOrPrecursorTupleIfOpaque(); |
| if (is_cross_origin_navigation && !compatible_with_error_page) { |
| commit_params_->force_new_document_sequence_number = true; |
| } |
| } |
| |
| if (early_hints_manager_) { |
| commit_params_->early_hints_preloaded_resources = |
| early_hints_manager_->TakePreloadedResourceURLs(); |
| } |
| |
| if (response_head_) { |
| commit_params_->navigation_delivery_type = |
| response_head_->navigation_delivery_type; |
| } |
| |
| // Add our map of modified blink runtime-enabled features to |
| // the commit params so they can be communicated to the renderer process. |
| commit_params_->modified_runtime_features = |
| runtime_feature_state_context_.GetFeatureOverrides(); |
| |
| // Documents loaded from fenced frame configs can opt into allowing |
| // cross-origin subframes to use their reporting metadata to send |
| // `reportEvent()` beacons. The cross-origin subframes still require a |
| // separate per-report opt-in. |
| if (fenced_frame_properties_.has_value() && response_head_ && |
| response_head_->parsed_headers->allow_cross_origin_event_reporting) { |
| fenced_frame_properties_->SetAllowCrossOriginEventReporting(); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesSrcPermissionsPolicy)) { |
| std::optional<url::Origin> mapped_origin; |
| if (fenced_frame_properties_.has_value()) { |
| mapped_origin = url::Origin::Create( |
| fenced_frame_properties_->mapped_url()->GetValueIgnoringVisibility()); |
| } else if (frame_tree_node_->HasFencedFrameProperties() && |
| frame_tree_node_->GetFencedFrameProperties()->mapped_url()) { |
| mapped_origin = |
| url::Origin::Create(frame_tree_node_->GetFencedFrameProperties() |
| ->mapped_url() |
| ->GetValueIgnoringVisibility()); |
| } |
| |
| // Container policy allowlists are first calculated by the embedder where |
| // origin of the fenced frame is opaque. Now that the mapped URL is known, |
| // update the container policy allowlists so that any allowlist with |
| // "matches_opaque_src=true" points to the final mapped origin. This will be |
| // the container policy that is sent to the inner root to construct the |
| // final permissions policy. |
| if (mapped_origin.has_value()) { |
| for (auto& declaration : commit_params_->frame_policy.container_policy) { |
| if (declaration.matches_opaque_src) { |
| CHECK(!declaration.self_if_matches.has_value()); |
| declaration.matches_opaque_src = false; |
| declaration.self_if_matches = mapped_origin.value(); |
| } |
| } |
| } |
| } |
| |
| // Create a view of the fenced frame properties from the perspective of the |
| // fenced frame content, which will be sent to its renderer. |
| // On each navigation commit within the fenced frame tree: |
| // * If the properties have no mapped url, the browser will send the renderer |
| // the `RedactedFencedFrameProperties` unconditionally. |
| // * If the properties do have a mapped url, the browser will send the |
| // renderer the `RedactedFencedFrameProperties`, redacting extra information |
| // based on whether the origin is same-origin to the urn's mapped_url (after |
| // redirects). |
| // This is because we want to make fenced frame APIs available only |
| // in same-origin contexts, when "same-origin" has a coherent definition. |
| const auto& computed_fenced_frame_properties = ComputeFencedFrameProperties(); |
| if (computed_fenced_frame_properties.has_value()) { |
| content::FencedFrameEntity entity = |
| content::FencedFrameEntity::kSameOriginContent; |
| if (computed_fenced_frame_properties->mapped_url().has_value() && |
| !origin_to_commit.IsSameOriginWith( |
| computed_fenced_frame_properties->mapped_url() |
| ->GetValueIgnoringVisibility())) { |
| entity = content::FencedFrameEntity::kCrossOriginContent; |
| } |
| commit_params_->fenced_frame_properties = |
| computed_fenced_frame_properties->RedactFor(entity); |
| } |
| |
| commit_params_->load_with_storage_access = ShouldLoadWithStorageAccess( |
| begin_params(), common_params(), frame_tree_node()->current_frame_host(), |
| did_encounter_cross_origin_redirect(), GetURL(), response()); |
| |
| auto common_params = common_params_->Clone(); |
| auto commit_params = commit_params_.Clone(); |
| auto response_head = response_head_.Clone(); |
| if (!subresource_loader_params_.prefetched_signed_exchanges.empty()) { |
| commit_params->prefetched_signed_exchanges = |
| std::move(subresource_loader_params_.prefetched_signed_exchanges); |
| } |
| |
| // TODO(https://crbug.com/40095391): Convert to CHECK if it proves to be |
| // consistently upheld condition. |
| DUMP_WILL_BE_CHECK(commit_params->redirect_response.size() == |
| commit_params->redirect_infos.size()); |
| // Before sending the commit parameters to the renderer process, sanitize |
| // the redirect URLs to avoid leaking potentially sensitive data into |
| // processes which are cross-site. There is no dependency on the |
| // cross-site-ness, therefore just sanitize unilaterally. |
| SanitizeRedirectsForCommit(commit_params); |
| |
| GetRenderFrameHost()->CommitNavigation( |
| this, std::move(common_params), std::move(commit_params), |
| std::move(response_head), std::move(response_body_), |
| std::move(url_loader_client_endpoints_), std::move(controller), |
| std::move(subresource_overrides_), |
| std::move(service_worker_container_info), document_token_, |
| devtools_navigation_token_); |
| if (service_worker_handle_ && |
| service_worker_handle_->service_worker_client()) { |
| service_worker_handle_->service_worker_client()->SetContainerReady(); |
| } |
| UpdateNavigationHandleTimingsOnCommitSent(); |
| |
| // Give SpareRenderProcessHostManager a heads-up about the most recently used |
| // BrowserContext. This is mostly needed to make sure the spare is warmed-up |
| // if it wasn't done in RenderProcessHostImpl::GetProcessHostForSiteInstance. |
| RenderProcessHostImpl::NotifySpareManagerAboutRecentlyUsedSiteInstance( |
| GetRenderFrameHost()->GetSiteInstance()); |
| |
| SendDeferredConsoleMessages(); |
| } |
| |
| void NavigationRequest::CommitPageActivation() { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::CommitPageActivation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| // An activation is either for the back-forward cache or prerendering. They |
| // are mutually exclusive. |
| DCHECK_NE(IsServedFromBackForwardCache(), IsPrerenderedPageActivation()); |
| |
| NavigationControllerImpl* controller = GetNavigationController(); |
| |
| if (IsServedFromBackForwardCache()) { |
| std::unique_ptr<BackForwardCacheImpl::Entry> activated_entry; |
| // Navigations served from the back-forward cache must be a history |
| // navigation, and thus should have a valid |pending_history_list_index| |
| // value. We will pass that value and the |current_history_list_length| |
| // value to update the history index and length information saved in the |
| // renderer, which might be stale. |
| DCHECK_GE(commit_params_->pending_history_list_index, 0); |
| |
| auto page_restore_params = blink::mojom::PageRestoreParams::New(); |
| page_restore_params->navigation_start = NavigationStart(); |
| page_restore_params->pending_history_list_index = |
| commit_params_->pending_history_list_index; |
| page_restore_params->current_history_list_length = |
| commit_params_->current_history_list_length; |
| activated_entry = controller->GetBackForwardCache().RestoreEntry( |
| nav_entry_id_, std::move(page_restore_params)); |
| CHECK(activated_entry); |
| |
| // Restore navigation API entries, since they will probably have changed |
| // since the page entered bfcache. We must update all frames, not just the |
| // top frame, because it is possible (though unlikely) that an iframe's |
| // entries have changed, too. |
| activated_entry->render_frame_host()->ForEachRenderFrameHostImplWithAction( |
| [this, &activated_entry](RenderFrameHostImpl* rfh) { |
| // |this| is given as a parameter to |
| // GetNavigationApiHistoryEntryVectors() only for the frame being |
| // committed (i.e., the top frame). |
| auto entry_arrays = |
| rfh->frame_tree() |
| ->controller() |
| .GetNavigationApiHistoryEntryVectors( |
| rfh->frame_tree_node(), |
| activated_entry->render_frame_host() == rfh ? this |
| : nullptr); |
| rfh->GetAssociatedLocalFrame() |
| ->SetNavigationApiHistoryEntriesForRestore( |
| std::move(entry_arrays), |
| blink::mojom::NavigationApiEntryRestoreReason::kBFCache); |
| return RenderFrameHost::FrameIterationAction::kContinue; |
| }); |
| |
| // When activating from BackForwardCache, properly set the |
| // BrowsingContextGroupSwap information. This is required because we |
| // otherwise do it in the RenderFrameHost selection, in |
| // GetFrameHostForNavigation, which does not happen for BFCache restores. |
| // TODO(crbug.com/40922919): This code assumes that pages can only be |
| // stored in the BFCache if they live in a different BrowsingInstance in |
| // another CoopRelatedGroup, so we enforce that invariant via a CHECK. If |
| // this is not the case anymore, `browsing_context_group_swap_` should be |
| // set to BrowsingContextGroupSwap::CreateRelatedCoopSwap() if the two |
| // SiteInstances live in the same CoopRelatedGroup. |
| SiteInstanceImpl* current_site_instance = |
| frame_tree_node_->current_frame_host()->GetSiteInstance(); |
| SiteInstanceImpl* target_site_instance = |
| activated_entry->render_frame_host()->GetSiteInstance(); |
| CHECK(!target_site_instance->IsRelatedSiteInstance(current_site_instance)); |
| browsing_context_group_swap_ = |
| BrowsingContextGroupSwap::CreateSecuritySwap(); |
| |
| base::WeakPtr<NavigationRequest> weak_self(weak_factory_.GetWeakPtr()); |
| ReadyToCommitNavigation(false /* is_error */); |
| // The call above might block on showing a user dialog. The interaction of |
| // the user with this dialog might result in the WebContents owning this |
| // NavigationRequest to be destroyed. Return if this is the case. |
| if (!weak_self) |
| return; |
| |
| // Treat this as the commit start time for the activation (i.e., after the |
| // ReadyToCommitNavigation call). |
| page_activation_commit_time_ = base::TimeTicks::Now(); |
| |
| // Use std::exchange instead of move, so that we clear out the optional on |
| // the commit_params. |
| activated_entry->SetViewTransitionState( |
| std::exchange(commit_params_->view_transition_state, {})); |
| |
| // Move the BackForwardCacheImpl::Entry into RenderFrameHostManager, in |
| // preparation for committing. This entry may be either restored from the |
| // backforward cache. |
| DCHECK(activated_entry); |
| frame_tree_node_->render_manager()->RestorePage( |
| activated_entry->TakeStoredPage()); |
| } else { |
| // Copy the prerender trigger type before PrerenderHost is destroyed in |
| // ActivateReservedHost(). |
| PreloadingTriggerType trigger_type = |
| GetPrerenderHostRegistry().GetPrerenderTriggerType( |
| prerender_frame_tree_node_id()); |
| const std::string embedder_histogram_suffix = |
| GetPrerenderHostRegistry().GetPrerenderEmbedderHistogramSuffix( |
| prerender_frame_tree_node_id()); |
| |
| std::unique_ptr<StoredPage> stored_page = |
| GetPrerenderHostRegistry().ActivateReservedHost( |
| prerender_frame_tree_node_id_.value(), *this); |
| CHECK(stored_page); |
| |
| RenderFrameHostImpl* rfh = stored_page->render_frame_host(); |
| |
| // Set the prerender trigger type and embedder histogram suffix for metrics. |
| set_prerender_trigger_type(trigger_type); |
| set_prerender_embedder_histogram_suffix(embedder_histogram_suffix); |
| |
| // The prerender page might have navigated. Update the URL and the redirect |
| // chain, as the prerendered page might have been redirected or performed |
| // a same-document navigation. |
| // TODO(crbug.com/40170496): Ensure that the tests that navigate |
| // MPArch activation flow do not crash. This is a hack to unblock the basic |
| // MPArch activation flow for now. There are probably other parameters which |
| // are out of sync, and we need to carefully think through how we can |
| // activate a RenderFrameHost whose URL doesn't match the one that was |
| // initially passed to NavigationRequest (or disallow subsequent navigations |
| // in the main frame of the prerender frame tree). |
| common_params_->url = rfh->GetLastCommittedURL(); |
| // TODO(crbug.com/40170496): We may have to add the entire redirect |
| // chain. |
| redirect_chain_.clear(); |
| redirect_chain_.push_back(rfh->GetLastCommittedURL()); |
| |
| base::WeakPtr<NavigationRequest> weak_self(weak_factory_.GetWeakPtr()); |
| ReadyToCommitNavigation(false /* is_error */); |
| // The call above might block on showing a user dialog. The interaction of |
| // the user with this dialog might result in the WebContents owning this |
| // NavigationRequest to be destroyed. Return if this is the case. |
| if (!weak_self) |
| return; |
| |
| // Treat this as the commit start time for the activation (i.e., after the |
| // ReadyToCommitNavigation call). |
| page_activation_commit_time_ = base::TimeTicks::Now(); |
| |
| // Use std::exchange instead of move, so that we clear out the optional on |
| // the commit_params. |
| stored_page->SetViewTransitionState( |
| std::exchange(commit_params_->view_transition_state, {})); |
| |
| // Update navigation API entries. A prerendered page has only a single |
| // history entry, but now it has access to a full back/forward list. |
| rfh->ForEachRenderFrameHostImplWithAction([this, &stored_page]( |
| RenderFrameHostImpl* rfh) { |
| // Currently, prerender activation only happens for DIFFERENT_DOCUMENT |
| // navigations. If that ever changes, `reason` calculation will need to be |
| // updated (and new NavigationApiEntryRestoreReason values added). |
| DCHECK_EQ(common_params_->navigation_type, |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT); |
| blink::mojom::NavigationApiEntryRestoreReason reason = |
| common_params_->should_replace_current_entry |
| ? blink::mojom::NavigationApiEntryRestoreReason:: |
| kPrerenderActivationReplace |
| : blink::mojom::NavigationApiEntryRestoreReason:: |
| kPrerenderActivationPush; |
| // |this| is given as a parameter to |
| // GetNavigationApiHistoryEntryVectors() only for the frame being |
| // committed (i.e., the top frame). |
| auto entry_arrays = |
| rfh->frame_tree()->controller().GetNavigationApiHistoryEntryVectors( |
| rfh->frame_tree_node(), |
| stored_page->render_frame_host() == rfh ? this : nullptr); |
| rfh->GetAssociatedLocalFrame()->SetNavigationApiHistoryEntriesForRestore( |
| std::move(entry_arrays), reason); |
| return RenderFrameHost::FrameIterationAction::kContinue; |
| }); |
| |
| // Move the StoredPage into RenderFrameHostManager, in |
| // preparation for committing. This entry may be used for prerendering. |
| frame_tree_node_->render_manager()->ActivatePrerender( |
| std::move(stored_page)); |
| } |
| |
| // Commit the page activation. This includes committing the RenderFrameHost |
| // and restoring extra state, such as proxies, etc. |
| // Note that this will delete the NavigationRequest. |
| GetRenderFrameHost()->DidCommitPageActivation( |
| this, IsPrerenderedPageActivation() |
| ? MakeDidCommitProvisionalLoadParamsForPrerenderActivation() |
| : MakeDidCommitProvisionalLoadParamsForBFCacheRestore()); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous call. |
| } |
| |
| void NavigationRequest::SetExpectedProcess( |
| RenderProcessHost* expected_process) { |
| if (expected_process && |
| expected_process->GetID() == expected_render_process_host_id_) { |
| // This |expected_process| has already been informed of the navigation, |
| // no need to update it again. |
| return; |
| } |
| |
| ResetExpectedProcess(); |
| |
| if (expected_process == nullptr) |
| return; |
| |
| // Keep track of the speculative RenderProcessHost and tell it to expect a |
| // navigation to |site_info_|. |
| expected_render_process_host_id_ = expected_process->GetID(); |
| expected_process->AddObserver(this); |
| RenderProcessHostImpl::AddExpectedNavigationToSite( |
| frame_tree_node()->navigator().controller().GetBrowserContext(), |
| expected_process, site_info_); |
| } |
| |
| void NavigationRequest::SetExpectedProcessIfAssociated() { |
| if (associated_rfh_type_ != AssociatedRenderFrameHostType::NONE) { |
| RenderFrameHostImpl* navigating_frame_host = |
| associated_rfh_type_ == AssociatedRenderFrameHostType::SPECULATIVE |
| ? frame_tree_node_->render_manager()->speculative_frame_host() |
| : frame_tree_node_->current_frame_host(); |
| SetExpectedProcess(navigating_frame_host->GetProcess()); |
| } |
| } |
| |
| void NavigationRequest::ResetExpectedProcess() { |
| if (expected_render_process_host_id_.is_null()) { |
| // No expected process is set, nothing to update. |
| return; |
| } |
| RenderProcessHost* process = |
| RenderProcessHost::FromID(expected_render_process_host_id_); |
| if (process) { |
| RenderProcessHostImpl::RemoveExpectedNavigationToSite( |
| frame_tree_node()->navigator().controller().GetBrowserContext(), |
| process, site_info_); |
| process->RemoveObserver(this); |
| } |
| expected_render_process_host_id_ = ChildProcessId(); |
| } |
| |
| void NavigationRequest::RenderProcessHostDestroyed(RenderProcessHost* host) { |
| DCHECK_EQ(host->GetID(), expected_render_process_host_id_); |
| ResetExpectedProcess(); |
| } |
| |
| void NavigationRequest::RenderProcessExited( |
| RenderProcessHost* host, |
| const ChildProcessTerminationInfo& info) {} |
| |
| void NavigationRequest::UpdateNavigationHandleTimingsOnResponseReceived( |
| bool is_redirect, |
| bool is_first_response) { |
| base::TimeTicks loader_callback_time = base::TimeTicks::Now(); |
| |
| const base::TimeDelta domain_lookup_delay = |
| response_head_->load_timing.connect_timing.domain_lookup_end - |
| response_head_->load_timing.connect_timing.domain_lookup_start; |
| const base::TimeDelta connect_delay = |
| response_head_->load_timing.connect_timing.connect_end - |
| response_head_->load_timing.connect_timing.connect_start; |
| const base::TimeDelta ssl_delay = |
| response_head_->load_timing.connect_timing.ssl_end - |
| response_head_->load_timing.connect_timing.ssl_start; |
| |
| if (is_first_response) { |
| CHECK(!navigation_handle_timing_.first_fetch_start_time.has_value()); |
| DCHECK(navigation_handle_timing_.first_request_start_time.is_null()); |
| DCHECK(navigation_handle_timing_.first_response_start_time.is_null()); |
| DCHECK(navigation_handle_timing_.first_loader_callback_time.is_null()); |
| |
| if (!response_head_->request_start.is_null()) { |
| navigation_handle_timing_.first_fetch_start_time = |
| response_head_->request_start; |
| } |
| navigation_handle_timing_.first_request_start_time = |
| response_head_->load_timing.send_start; |
| navigation_handle_timing_.first_response_start_time = |
| response_head_->load_timing.receive_headers_start; |
| navigation_handle_timing_.first_loader_callback_time = loader_callback_time; |
| |
| navigation_handle_timing_.first_request_domain_lookup_delay = |
| domain_lookup_delay; |
| navigation_handle_timing_.first_request_connect_delay = connect_delay; |
| navigation_handle_timing_.first_request_ssl_delay = ssl_delay; |
| |
| first_fetch_start_time_ = response_head_->request_start; |
| } |
| |
| if (!is_redirect) { |
| navigation_handle_timing_.non_redirected_request_start_time = |
| response_head_->load_timing.send_start; |
| navigation_handle_timing_.non_redirect_response_start_time = |
| response_head_->load_timing.receive_headers_start; |
| navigation_handle_timing_.non_redirect_response_loader_callback_time = |
| loader_callback_time; |
| } |
| |
| navigation_handle_timing_.final_request_start_time = |
| response_head_->load_timing.send_start; |
| navigation_handle_timing_.final_response_start_time = |
| response_head_->load_timing.receive_headers_start; |
| navigation_handle_timing_.final_non_informational_response_start_time = |
| response_head_->load_timing.receive_non_informational_headers_start; |
| navigation_handle_timing_.final_loader_callback_time = loader_callback_time; |
| navigation_handle_timing_.final_request_domain_lookup_delay = |
| domain_lookup_delay; |
| navigation_handle_timing_.final_request_connect_delay = connect_delay; |
| navigation_handle_timing_.final_request_ssl_delay = ssl_delay; |
| |
| if (response_head_->load_timing_internal_info) { |
| navigation_handle_timing_.create_stream_delay = |
| response_head_->load_timing_internal_info->create_stream_delay; |
| navigation_handle_timing_.connected_callback_delay = |
| response_head_->load_timing_internal_info->connected_callback_delay; |
| navigation_handle_timing_.initialize_stream_delay = |
| response_head_->load_timing_internal_info->initialize_stream_delay; |
| // Reset `load_timing_internal_info` to make sure that isn't exposed. |
| response_head_->load_timing_internal_info.reset(); |
| } |
| |
| final_receive_headers_end_time_ = |
| response_head_->load_timing.receive_headers_end; |
| |
| // |navigation_commit_sent_time| will be updated by |
| // UpdateNavigationHandleTimingsOnCommitSent() later. |
| DCHECK(navigation_handle_timing_.navigation_commit_sent_time.is_null()); |
| |
| GetDelegate()->DidUpdateNavigationHandleTiming(this); |
| } |
| |
| void NavigationRequest::UpdateNavigationHandleTimingsOnCommitSent() { |
| DCHECK(navigation_handle_timing_.navigation_commit_sent_time.is_null()); |
| navigation_handle_timing_.navigation_commit_sent_time = |
| base::TimeTicks::Now(); |
| |
| GetDelegate()->DidUpdateNavigationHandleTiming(this); |
| } |
| |
| void NavigationRequest::UpdateSiteInfo( |
| RenderProcessHost* post_redirect_process) { |
| ChildProcessId post_redirect_process_id; |
| if (post_redirect_process) { |
| post_redirect_process_id = post_redirect_process->GetID(); |
| } |
| |
| SiteInfo new_site_info = GetSiteInfoForCommonParamsURL(); |
| if (new_site_info == site_info_ && |
| post_redirect_process_id == expected_render_process_host_id_) { |
| return; |
| } |
| |
| // Stop expecting a navigation to the current SiteInfo in the current expected |
| // process. |
| ResetExpectedProcess(); |
| |
| // Update the SiteInfo and the expected process. |
| site_info_ = new_site_info; |
| SetExpectedProcess(post_redirect_process); |
| } |
| |
| bool NavigationRequest::IsAllowedByCSPDirective( |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& policies, |
| network::CSPContext* context, |
| network::mojom::CSPDirectiveName directive, |
| bool has_followed_redirect, |
| bool url_upgraded_after_redirect, |
| bool is_opaque_fenced_frame, |
| network::CSPContext::CheckCSPDisposition disposition) { |
| GURL url; |
| // If this request was upgraded in the net stack, downgrade the URL back to |
| // HTTP before checking report only policies. |
| if (url_upgraded_after_redirect && |
| disposition == |
| network::CSPContext::CheckCSPDisposition::CHECK_REPORT_ONLY_CSP && |
| common_params_->url.SchemeIs(url::kHttpsScheme)) { |
| GURL::Replacements replacements; |
| replacements.SetSchemeStr(url::kHttpScheme); |
| url = common_params_->url.ReplaceComponents(replacements); |
| } else { |
| url = common_params_->url; |
| } |
| network::CSPCheckResult result = context->IsAllowedByCsp( |
| policies, directive, url, commit_params_->original_url, |
| has_followed_redirect, common_params_->source_location, disposition, |
| begin_params_->is_form_submission, is_opaque_fenced_frame); |
| if (result.WouldBlockIfWildcardDoesNotMatchWs()) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| GetParentFrame(), |
| blink::mojom::WebFeature::kCspWouldBlockIfWildcardDoesNotMatchWs); |
| } |
| return result.IsAllowed(); |
| } |
| |
| net::Error NavigationRequest::CheckCSPDirectives( |
| RenderFrameHostCSPContext parent_context, |
| const PolicyContainerPolicies* parent_policies, |
| RenderFrameHostCSPContext initiator_context, |
| const PolicyContainerPolicies* initiator_policies, |
| bool has_followed_redirect, |
| bool url_upgraded_after_redirect, |
| bool is_response_check, |
| network::CSPContext::CheckCSPDisposition disposition) { |
| // Following directive checks' order is important as the `error` code takes |
| // only the result last set. |
| net::Error error = net::OK; |
| |
| if (initiator_policies) { |
| // [form-action] |
| if (begin_params_->is_form_submission && !is_response_check && |
| !IsAllowedByCSPDirective( |
| initiator_policies->content_security_policies, &initiator_context, |
| network::mojom::CSPDirectiveName::FormAction, has_followed_redirect, |
| url_upgraded_after_redirect, |
| /*is_opaque_fenced_frame=*/false, disposition)) { |
| // net::ERR_ABORTED is used instead of net::ERR_BLOCKED_BY_CSP. This is |
| // a better user experience as the user is not presented with an error |
| // page. However if other CSP directives like frame-src are violated, it |
| // may be appropriate for them to use ERR_BLOCKED_BY_CSP so this can be |
| // overridden by the checks below. |
| error = net::ERR_ABORTED; |
| } |
| } |
| |
| // [frame-src] or [fenced-frame-src] |
| if (parent_policies) { |
| bool is_opaque_fenced_frame_root_navigation = |
| frame_tree_node_->IsFencedFrameRoot() && |
| fenced_frame_properties_.has_value() && |
| fenced_frame_properties_->mapped_url().has_value() && |
| !fenced_frame_properties_->mapped_url() |
| ->GetValueForEntity(FencedFrameEntity::kEmbedder) |
| .has_value(); |
| if (!IsAllowedByCSPDirective( |
| parent_policies->content_security_policies, &parent_context, |
| frame_tree_node_->IsFencedFrameRoot() |
| ? network::mojom::CSPDirectiveName::FencedFrameSrc |
| : network::mojom::CSPDirectiveName::FrameSrc, |
| has_followed_redirect, url_upgraded_after_redirect, |
| is_opaque_fenced_frame_root_navigation, disposition)) { |
| error = net::ERR_BLOCKED_BY_CSP; |
| } |
| } |
| |
| return error; |
| } |
| |
| net::Error NavigationRequest::CheckContentSecurityPolicy( |
| bool has_followed_redirect, |
| bool url_upgraded_after_redirect, |
| bool is_response_check) { |
| DCHECK(policy_container_builder_.has_value()); |
| if (common_params_->url.SchemeIs(url::kAboutScheme)) |
| return net::OK; |
| |
| if (IsSameDocument()) |
| return net::OK; |
| |
| if (common_params_->should_check_main_world_csp == |
| network::mojom::CSPDisposition::DO_NOT_CHECK) { |
| return net::OK; |
| } |
| |
| RenderFrameHostImpl* parent = frame_tree_node()->parent(); |
| const PolicyContainerPolicies* parent_policies = |
| policy_container_builder_->ParentPolicies(); |
| DCHECK(!parent == !parent_policies); |
| bool set_parent_for_nested_frame_tree = |
| !parent && frame_tree_node()->IsFencedFrameRoot() && |
| frame_tree_node()->render_manager()->GetOuterDelegateNode(); |
| if (set_parent_for_nested_frame_tree) { |
| parent = frame_tree_node() |
| ->render_manager() |
| ->GetOuterDelegateNode() |
| ->current_frame_host() |
| ->GetParent(); |
| // TODO(antoniosartori): If we want to keep checking frame-src for fenced |
| // frames, consider storing a snapshot of the parent policies in the |
| // `policy_container_builder_` at the beginning of the navigation. |
| parent_policies = &parent->policy_container_host()->policies(); |
| } |
| |
| const PolicyContainerPolicies* initiator_policies = |
| policy_container_builder_->InitiatorPolicies(); |
| |
| // CSP checking happens in three phases, per steps 3-5 of |
| // https://fetch.spec.whatwg.org/#main-fetch: |
| // |
| // (1) Check report-only policies and trigger reports for any violations. |
| // (2) Upgrade the request to HTTPS if necessary. |
| // (3) Check enforced policies (triggering reports for any violations of those |
| // policies) and block the request if necessary. |
| // |
| // This sequence of events allows site owners to learn about (via step 1) any |
| // requests that are upgraded in step 2. |
| |
| RenderFrameHostCSPContext parent_context(parent); |
| |
| // Note: the initiator RenderFrameHost could have been deleted by |
| // now. Then this RenderFrameHostCSPContext will do nothing and we won't |
| // report violations for this check. |
| // |
| // If the initiator frame has navigated away in between, we also use a no-op |
| // `initiator_csp_context`, in order not to trigger `securitypolicyviolation` |
| // events in the wrong document. |
| RenderFrameHostCSPContext initiator_context( |
| GetInitiatorDocumentRenderFrameHost()); |
| |
| net::Error report_only_csp_status = CheckCSPDirectives( |
| parent_context, parent_policies, initiator_context, initiator_policies, |
| has_followed_redirect, url_upgraded_after_redirect, is_response_check, |
| network::CSPContext::CHECK_REPORT_ONLY_CSP); |
| |
| // upgrade-insecure-requests is handled in the network code for redirects, |
| // only do the upgrade here if this is not a redirect. |
| // Note that `FrameTreeNode::IsMainFrame()` returns true for fenced frames |
| // based on MPArch, but it's fine to skip the logic below as |
| // `network::UpgradeInsecureRequest()` does not apply to fenced frame |
| // navigation requests. (See https://github.com/WICG/fenced-frame/issues/23) |
| if (!has_followed_redirect && !frame_tree_node()->IsMainFrame()) { |
| DCHECK(parent_policies); |
| if (parent_policies && network::ShouldUpgradeInsecureRequest( |
| parent_policies->content_security_policies)) { |
| upgrade_if_insecure_ = true; |
| network::UpgradeInsecureRequest(&common_params_->url); |
| common_params_->referrer = Referrer::SanitizeForRequest( |
| common_params_->url, *common_params_->referrer); |
| commit_params_->original_url = common_params_->url; |
| } |
| } |
| |
| net::Error enforced_csp_status = CheckCSPDirectives( |
| parent_context, parent_policies, initiator_context, initiator_policies, |
| has_followed_redirect, url_upgraded_after_redirect, is_response_check, |
| network::CSPContext::CHECK_ENFORCED_CSP); |
| if (enforced_csp_status != net::OK) |
| return enforced_csp_status; |
| return report_only_csp_status; |
| } |
| |
| NavigationRequest::CredentialedSubresourceCheckResult |
| NavigationRequest::CheckCredentialedSubresource() const { |
| // It only applies to subframes. |
| if (frame_tree_node_->IsOutermostMainFrame()) |
| return CredentialedSubresourceCheckResult::ALLOW_REQUEST; |
| |
| // URLs with no embedded credentials should load correctly. |
| if (!common_params_->url.has_username() && |
| !common_params_->url.has_password()) |
| return CredentialedSubresourceCheckResult::ALLOW_REQUEST; |
| |
| // Relative URLs on top-level pages that were loaded with embedded credentials |
| // should load correctly. |
| RenderFrameHostImpl* parent = frame_tree_node_->GetParentOrOuterDocument(); |
| DCHECK(parent); |
| const GURL& parent_url = parent->GetLastCommittedURL(); |
| if (url::IsSameOriginWith(parent_url, common_params_->url) && |
| parent_url.username() == common_params_->url.username() && |
| parent_url.password() == common_params_->url.password()) { |
| return CredentialedSubresourceCheckResult::ALLOW_REQUEST; |
| } |
| |
| // Warn the user about the request being blocked. |
| const char* console_message = |
| "Subresource requests whose URLs contain embedded credentials (e.g. " |
| "`https://user:pass@host/`) are blocked. See " |
| "https://www.chromestatus.com/feature/5669008342777856 for more " |
| "details."; |
| parent->AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kWarning, |
| console_message); |
| return CredentialedSubresourceCheckResult::BLOCK_REQUEST; |
| } |
| |
| NavigationRequest::AboutSrcDocCheckResult NavigationRequest::CheckAboutSrcDoc() |
| const { |
| if (!common_params_->url.IsAboutSrcdoc()) |
| return AboutSrcDocCheckResult::ALLOW_REQUEST; |
| |
| // Loading about:srcdoc in the main frame can't have any reasonable meaning. |
| // There might be a malicious website trying to exploit a bug from this. As a |
| // defensive measure, do not proceed. They would have failed anyway later. |
| if (frame_tree_node_->IsMainFrame()) |
| return AboutSrcDocCheckResult::BLOCK_REQUEST; |
| |
| // There are 4 cases where we allow a navigation to about:srcdoc: |
| |
| // 1) We allow same-document navigations from any frame. |
| if (IsSameDocument()) { |
| return AboutSrcDocCheckResult::ALLOW_REQUEST; |
| } |
| |
| const std::optional<url::Origin>& initiator_origin = |
| common_params().initiator_origin; |
| // 2) Browser-initiated navigations are (temporarily) allowed for |
| // about:srcdoc. |
| if (!initiator_origin) { |
| // TODO(https://crbug.com/40165505): for now, allow this, and land the |
| // change to block it in a separate CL in case it breaks things beyond our |
| // local test suites. |
| return AboutSrcDocCheckResult::ALLOW_REQUEST; |
| } |
| |
| // 3) An about:srcdoc frame can reload itself (even if it is cross-origin from |
| // its parent due to being sandboxed). |
| if (frame_tree_node() |
| ->current_frame_host() |
| ->GetLastCommittedURL() |
| .IsAboutSrcdoc() && |
| initiator_origin->IsSameOriginWith( |
| frame_tree_node()->current_frame_host()->GetLastCommittedOrigin())) { |
| return AboutSrcDocCheckResult::ALLOW_REQUEST; |
| } |
| |
| // 4) Setting src = 'about:srcdoc' is allowed for now as long as the |
| // initiator's origin matches the origin of the srcdoc's parent. It is |
| // important not to allow initiators that are cross-origin with the parent, |
| // because the content comes from the parent and many places in the code |
| // assume the origin comes from the initiator. |
| // TODO(https://crbug.com/40165505): navigations to 'about:srcdoc' aren't |
| // supposed to ever be allowed according to spec. |
| if (*initiator_origin == |
| frame_tree_node()->parent()->GetLastCommittedOrigin()) { |
| return AboutSrcDocCheckResult::ALLOW_REQUEST; |
| } |
| |
| // Navigations with an initiator that is cross-origin to the about:srcdoc |
| // parent are not allowed. |
| return AboutSrcDocCheckResult::BLOCK_REQUEST; |
| } |
| |
| void NavigationRequest::SetupCSPEmbeddedEnforcement() { |
| if (IsInMainFrame()) |
| return; |
| // TODO(https://crbug.com/11129645): MHTML iframe not supported yet. |
| if (IsForMhtmlSubframe()) |
| return; |
| |
| // TODO(antoniosartori): Probably we should have taken a snapshot of the 'csp' |
| // attribute at the beginning of the navigation and not now, since the |
| // beforeunload handlers might have modified it in the meantime. |
| // See pull request about the spec: |
| // https://github.com/w3c/webappsec-cspee/pull/11 |
| network::mojom::ContentSecurityPolicyPtr frame_csp_attribute = |
| frame_tree_node()->csp_attribute() |
| ? frame_tree_node()->csp_attribute()->Clone() |
| : nullptr; |
| if (frame_csp_attribute) { |
| // TODO(antoniosartori): Maybe we should revisit what 'self' means in the |
| // 'csp' attribute. |
| const GURL& url = GetURL(); |
| frame_csp_attribute->self_origin = network::mojom::CSPSource::New( |
| url.scheme(), url.host(), url.EffectiveIntPort(), "", false, false); |
| } |
| |
| const network::mojom::ContentSecurityPolicy* parent_required_csp = |
| frame_tree_node()->parent()->required_csp(); |
| |
| std::vector<network::mojom::ContentSecurityPolicyPtr> frame_csp; |
| frame_csp.push_back(std::move(frame_csp_attribute)); |
| std::string error_message; |
| if (network::IsValidRequiredCSPAttr(frame_csp, parent_required_csp, |
| error_message)) { |
| // If |frame_csp| is valid then it is not null. |
| SetRequiredCSP(std::move(frame_csp[0])); |
| return; |
| } |
| |
| if (frame_csp[0]) { |
| GetParentFrame()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf("The frame 'csp' attribute ('%s') is invalid and " |
| "will be discarded: %s", |
| frame_csp[0]->header->header_value.c_str(), |
| error_message.c_str())); |
| } |
| |
| if (parent_required_csp) { |
| SetRequiredCSP(parent_required_csp->Clone()); |
| } |
| // TODO(antoniosartori): Consider instead blocking the navigation here, |
| // since this seems to be insecure |
| // (cf. https://github.com/w3c/webappsec-cspee/pull/11). |
| } |
| |
| NavigationRequest::CSPEmbeddedEnforcementResult |
| NavigationRequest::CheckCSPEmbeddedEnforcement() { |
| // We enforce CSPEE only for subframes. |
| if (IsInMainFrame()) |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| |
| if (IsSameDocument()) |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| |
| if (!required_csp_) |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| |
| // The |response()| can be null for navigations that do not require a |
| // URLLoader (about:blank, about:srcdoc, ...) |
| const network::mojom::AllowCSPFromHeaderValue* allow_csp_from = |
| response() ? response()->parsed_headers->allow_csp_from.get() : nullptr; |
| |
| if (network::AllowsBlanketEnforcementOfRequiredCSP( |
| GetParentFrame()->GetLastCommittedOrigin(), GetURL(), allow_csp_from, |
| required_csp_)) { |
| // Enforce the required CSPs on the frame by passing them down to blink. |
| policy_container_builder_->AddContentSecurityPolicy(required_csp_->Clone()); |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| } |
| |
| // All the URLs that do not |NeedsUrlLoader()| allows blanket enforcement of |
| // CSP, Except for MHTML iframe. |
| // TODO(arthursonzogni): Make MHTML response to use the normal loading path, |
| // by introducing their own MHTML UrlLoader. Then CSPEE can be supported. |
| if (!response()) { |
| // TODO(https://crbug.com/11129645): Remove MHTML edge case, once MHTML |
| // documents are handled through the standard code path with its own |
| // URLLoaderFactory. |
| CHECK(IsForMhtmlSubframe()); |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| } |
| |
| std::string sanitized_blocked_url = |
| GetRedirectChain().front().DeprecatedGetOriginAsURL().spec(); |
| if (allow_csp_from && allow_csp_from->is_error_message()) { |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf("The value of the 'Allow-CSP-From' response header " |
| "returned by %s is invalid: %s", |
| sanitized_blocked_url.c_str(), |
| allow_csp_from->get_error_message().c_str())); |
| } |
| |
| if (network::Subsumes(*required_csp_, |
| response()->parsed_headers->content_security_policy)) { |
| return CSPEmbeddedEnforcementResult::ALLOW_RESPONSE; |
| } |
| |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf( |
| "Refused to display '%s' in a frame. The embedder requires it to " |
| "enforce the following Content Security Policy: '%s'. However, the " |
| "frame neither accepts that policy using the Allow-CSP-From header " |
| "nor delivers a Content Security Policy which is at least as strong " |
| "as that one.", |
| sanitized_blocked_url.c_str(), |
| required_csp_->header->header_value.c_str())); |
| |
| return CSPEmbeddedEnforcementResult::BLOCK_RESPONSE; |
| } |
| |
| void NavigationRequest::UpdateHistoryParamsInCommitNavigationParams() { |
| NavigationController& navigation_controller = |
| frame_tree_node_->navigator().controller(); |
| commit_params_->current_history_list_index = |
| navigation_controller.GetCurrentEntryIndex(); |
| commit_params_->current_history_list_length = |
| navigation_controller.GetEntryCount(); |
| } |
| |
| void NavigationRequest::SanitizeRedirectsForCommit( |
| blink::mojom::CommitNavigationParamsPtr& commit_params) { |
| if (!base::FeatureList::IsEnabled(kSanitizeRedirectUrlsDuringNavigation)) { |
| return; |
| } |
| // It is safe to convert GURL to an Origin and back in the code below because |
| // we only want to discard the rest of the URL (e.g., path and params). The |
| // actual underlying Origin is not needed, which could be inherited or opaque |
| // in sandbox cases. |
| for (GURL& redirect : commit_params->redirects) { |
| redirect = redirect.DeprecatedGetOriginAsURL(); |
| } |
| |
| // In the redirect_infos vector, the last entry is the URL we are going to |
| // commit after following all redirects. We should not be sanitizing it, as |
| // we need to commit the real URL as part of the navigation. |
| if (!commit_params->redirect_infos.empty()) { |
| auto redirect_infos_span = base::span(commit_params->redirect_infos); |
| for (net::RedirectInfo& redirect : |
| redirect_infos_span.first(redirect_infos_span.size() - 1)) { |
| redirect.new_url = redirect.new_url.DeprecatedGetOriginAsURL(); |
| } |
| } |
| } |
| |
| void NavigationRequest::RendererRequestedNavigationCancellationForTesting() { |
| OnNavigationClientDisconnected(0, ""); |
| } |
| |
| void NavigationRequest::OnNavigationClientDisconnected( |
| uint32_t reason, |
| const std::string& description) { |
| // Renderer-initiated navigation cancellations can only happen before the |
| // navigation gets into the READY_TO_COMMIT state, because |
| // RendererCancellationThrottle will prevent renderer-initiated navigations |
| // from entering that state before the JS task that started the navigation |
| // finishes. After navigation reaches READY_TO_COMMIT stage, we should |
| // ignore these navigation cancellations, except for these two cases: |
| // 1. It reuses the current RenderFrame(Host), because the RenderFrame expects |
| // the navigation to be cancelled successfully (as the state in the renderer |
| // is already updated to cancel the navigation). |
| // TODO(crbug.com/40615943): This case will eventually go away with |
| // RenderDocument as cross-document navigations won't reuse RenderFrameHosts |
| // anymore. Fix tests that expect this behavior. |
| // 2. The target renderer had crashed, so the speculative RenderFrame is not |
| // live anymore, because the navigation can't commit in a crashed renderer. |
| std::optional<NavigationDiscardReason> discard_reason; |
| if (HasRenderFrameHost() && !GetRenderFrameHost()->IsRenderFrameLive()) { |
| discard_reason = NavigationDiscardReason::kRenderProcessGone; |
| } else { |
| switch (static_cast<mojom::NavigationClientDisconnectReason>(reason)) { |
| case mojom::NavigationClientDisconnectReason::kResetForSwap: |
| // If the RenderFrame that initiated this navigation request is swapped |
| // out (disconnecting its NavigationClient for this request), do not |
| // treat it as a cancellation. Otherwise, if a previous navigation |
| // before `this` is slow to commit, it would unexpectedly cancel `this` |
| // subsequent attempt to navigate elsewhere. |
| return; |
| case mojom::NavigationClientDisconnectReason::kNoExplicitReason: |
| discard_reason = NavigationDiscardReason::kInternalCancellation; |
| break; |
| case mojom::NavigationClientDisconnectReason::kResetForAbort: |
| discard_reason = NavigationDiscardReason::kExplicitCancellation; |
| break; |
| case mojom::NavigationClientDisconnectReason::kResetForNewNavigation: |
| discard_reason = |
| NavigationDiscardReason::kNewOtherNavigationRendererInitiated; |
| break; |
| case mojom::NavigationClientDisconnectReason:: |
| kResetForDuplicateNavigation: |
| discard_reason = NavigationDiscardReason::kNewDuplicateNavigation; |
| break; |
| } |
| if (!discard_reason.has_value()) { |
| // TODO(https://crbug.com/366060351): An invalid value was used. Kill |
| // either the requesting or committing client's process. |
| return; |
| } |
| } |
| |
| if (!IsWaitingToCommit()) { |
| // The cancellation happens before READY_TO_COMMIT. |
| frame_tree_node_->navigator().CancelNavigation(frame_tree_node_, |
| discard_reason.value()); |
| } else if (GetRenderFrameHost() == |
| frame_tree_node_->render_manager()->current_frame_host() || |
| !GetRenderFrameHost()->IsRenderFrameLive()) { |
| // If the NavigationRequest has already reached READY_TO_COMMIT, |
| // `render_frame_host_` owns `this`. Cache any needed state in stack |
| // variables to avoid a use-after-free. |
| FrameTreeNode* frame_tree_node = frame_tree_node_; |
| GetRenderFrameHost()->NavigationRequestCancelled(this, |
| discard_reason.value()); |
| // Ensure that the speculative RFH, if any, is also cleaned up. |
| frame_tree_node->render_manager()->DiscardSpeculativeRFHIfUnused( |
| discard_reason.value()); |
| } |
| |
| // Do not add code after this, NavigationRequest might have been destroyed. |
| } |
| |
| void NavigationRequest::HandleInterfaceDisconnection( |
| mojo::AssociatedRemote<mojom::NavigationClient>& navigation_client) { |
| // `Unretained()` is safe because the `mojo::AssociatedRemote` reference only |
| // refers to fields owned by `this`. |
| navigation_client.set_disconnect_with_reason_handler( |
| base::BindOnce(&NavigationRequest::OnNavigationClientDisconnected, |
| base::Unretained(this))); |
| } |
| |
| void NavigationRequest::IgnoreInterfaceDisconnection() { |
| return request_navigation_client_.set_disconnect_handler(base::DoNothing()); |
| } |
| |
| void NavigationRequest::IgnoreCommitInterfaceDisconnection() { |
| return commit_navigation_client_.set_disconnect_handler(base::DoNothing()); |
| } |
| |
| bool NavigationRequest::IsSameDocument() const { |
| return NavigationTypeUtils::IsSameDocument(common_params_->navigation_type); |
| } |
| |
| bool NavigationRequest::IsHistory() const { |
| return NavigationTypeUtils::IsHistory(common_params_->navigation_type); |
| } |
| |
| bool NavigationRequest::IsRestore() const { |
| return NavigationTypeUtils::IsRestore(common_params_->navigation_type); |
| } |
| |
| bool NavigationRequest::IsReload() const { |
| return NavigationTypeUtils::IsReload(common_params_->navigation_type); |
| } |
| |
| void NavigationRequest::RecordDownloadUseCountersPrePolicyCheck() { |
| RenderFrameHost* rfh = frame_tree_node_->current_frame_host(); |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadPrePolicyCheck); |
| |
| // Log UseCounters for opener navigations. |
| if (download_policy().IsType( |
| blink::NavigationDownloadType::kOpenerCrossOrigin)) { |
| rfh->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf( |
| "Navigating a cross-origin opener to a download (%s) is " |
| "deprecated, see " |
| "https://www.chromestatus.com/feature/5742188281462784.", |
| common_params_->url.spec().c_str())); |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kOpenerNavigationDownloadCrossOrigin); |
| } |
| |
| // Log UseCounters for download in sandbox. |
| if (download_policy().IsType(blink::NavigationDownloadType::kSandbox)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadInSandbox); |
| } |
| |
| // Log UseCounters for download without user activation. |
| if (download_policy().IsType(blink::NavigationDownloadType::kNoGesture)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadWithoutUserGesture); |
| } |
| |
| // Log UseCounters for download in ad frame without user activation. |
| if (download_policy().IsType( |
| blink::NavigationDownloadType::kAdFrameNoGesture)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadInAdFrameWithoutUserGesture); |
| } |
| |
| // Log UseCounters for download in ad frame. |
| if (download_policy().IsType(blink::NavigationDownloadType::kAdFrame)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadInAdFrame); |
| } |
| } |
| |
| void NavigationRequest::RecordDownloadUseCountersPostPolicyCheck() { |
| DCHECK(is_download_); |
| RenderFrameHost* rfh = frame_tree_node_->current_frame_host(); |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| rfh, blink::mojom::WebFeature::kDownloadPostPolicyCheck); |
| } |
| |
| void NavigationRequest::OnNavigationEventProcessed( |
| NavigationThrottleEvent event, |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_NE(NavigationThrottle::DEFER, result.action()); |
| switch (event) { |
| case NavigationThrottleEvent::kNoEvent: |
| DUMP_WILL_BE_NOTREACHED(); |
| return; |
| case NavigationThrottleEvent::kWillStartRequest: |
| OnWillStartRequestProcessed(result); |
| return; |
| case NavigationThrottleEvent::kWillRedirectRequest: |
| OnWillRedirectRequestProcessed(result); |
| return; |
| case NavigationThrottleEvent::kWillFailRequest: |
| OnWillFailRequestProcessed(result); |
| return; |
| case NavigationThrottleEvent::kWillProcessResponse: |
| OnWillProcessResponseProcessed(result); |
| return; |
| case NavigationThrottleEvent::kWillCommitWithoutUrlLoader: |
| OnWillCommitWithoutUrlLoaderProcessed(result); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void NavigationRequest::OnWillStartRequestProcessed( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_EQ(WILL_START_REQUEST, state_); |
| DCHECK_NE(NavigationThrottle::BLOCK_RESPONSE, result.action()); |
| DCHECK(processing_navigation_throttle_); |
| processing_navigation_throttle_ = false; |
| if (result.action() != NavigationThrottle::PROCEED) |
| SetState(CANCELING); |
| |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| |
| if (MaybeEvictFromBackForwardCacheBySubframeNavigation( |
| frame_tree_node_->current_frame_host())) { |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| return; |
| } |
| |
| OnStartChecksComplete(result); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest::OnWillRedirectRequestProcessed( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_EQ(WILL_REDIRECT_REQUEST, state_); |
| DCHECK_NE(NavigationThrottle::BLOCK_RESPONSE, result.action()); |
| DCHECK(processing_navigation_throttle_); |
| processing_navigation_throttle_ = false; |
| if (result.action() == NavigationThrottle::PROCEED) { |
| // Notify the delegate that a redirect was encountered and will be followed. |
| if (GetDelegate()) { |
| #if DCHECK_IS_ON() |
| DCHECK(is_safe_to_delete_); |
| base::AutoReset<bool> resetter(&is_safe_to_delete_, false); |
| #endif |
| GetDelegate()->DidRedirectNavigation(this); |
| } |
| } else { |
| SetState(CANCELING); |
| } |
| |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| OnRedirectChecksComplete(result); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest::OnWillFailRequestProcessed( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_EQ(WILL_FAIL_REQUEST, state_); |
| DCHECK_NE(NavigationThrottle::BLOCK_RESPONSE, result.action()); |
| DCHECK(processing_navigation_throttle_); |
| processing_navigation_throttle_ = false; |
| if (result.action() == NavigationThrottle::PROCEED) { |
| result = NavigationThrottle::ThrottleCheckResult( |
| NavigationThrottle::PROCEED, net_error_); |
| } else { |
| SetState(CANCELING); |
| } |
| |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| OnFailureChecksComplete(result); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest::OnWillProcessResponseProcessed( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_EQ(WILL_PROCESS_RESPONSE, state_); |
| DCHECK_NE(NavigationThrottle::BLOCK_REQUEST, result.action()); |
| DCHECK_NE(NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE, result.action()); |
| DCHECK(processing_navigation_throttle_); |
| processing_navigation_throttle_ = false; |
| if (result.action() != NavigationThrottle::PROCEED) { |
| SetState(CANCELING); |
| } |
| |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| OnWillProcessResponseChecksComplete(result); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest::OnWillCommitWithoutUrlLoaderProcessed( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK_EQ(WILL_COMMIT_WITHOUT_URL_LOADER, state_); |
| DCHECK(result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::PROCEED); |
| DCHECK(processing_navigation_throttle_); |
| processing_navigation_throttle_ = false; |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| |
| if (MaybeEvictFromBackForwardCacheBySubframeNavigation( |
| frame_tree_node_->current_frame_host())) { |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| return; |
| } |
| |
| OnWillCommitWithoutUrlLoaderChecksComplete(result); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| RenderFrameHostImpl* |
| NavigationRequest::GetRenderFrameHostRestoredFromBackForwardCache() const { |
| if (IsServedFromBackForwardCache()) { |
| return &*rfh_restored_from_back_forward_cache_.value(); |
| } |
| return nullptr; |
| } |
| |
| NavigatorDelegate* NavigationRequest::GetDelegate() const { |
| return frame_tree_node()->navigator().GetDelegate(); |
| } |
| |
| void NavigationRequest::Resume(NavigationThrottle* resuming_throttle) { |
| DCHECK(resuming_throttle); |
| CHECK(!is_resuming_) << "This call does not support re-entrancy."; |
| EnterChildTraceEvent("Resume", this); |
| |
| if (1u == throttle_registry_->GetDeferringThrottles().size()) { |
| is_resuming_ = true; |
| |
| // Stop watching for response body changes to ensure that the response body |
| // callback isn't called later in the throttle's lifetime with a response |
| // body that is not relevant to the throttle. |
| if (response_body_watcher_) { |
| CHECK(response_body_callback_); |
| response_body_watcher_.reset(); |
| base::WeakPtr<NavigationRequest> this_ptr(weak_factory_.GetWeakPtr()); |
| std::move(response_body_callback_).Run(std::string()); |
| if (this_ptr.WasInvalidated()) { |
| // TODO(https://crbug.com/411238078): Replace the debug code with a |
| // comment once we ensure that this is the root cause. |
| SCOPED_CRASH_KEY_STRING32("Bug411238078", "throttle", |
| resuming_throttle->GetNameForLogging()); |
| base::debug::DumpWithoutCrashing(); |
| return; |
| } |
| } |
| |
| is_resuming_ = false; |
| } |
| throttle_registry_->ResumeProcessingNavigationEvent(resuming_throttle); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationHandle might have been deleted |
| // by the previous call. |
| } |
| |
| void NavigationRequest::CancelDeferredNavigation( |
| NavigationThrottle* cancelling_throttle, |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK(cancelling_throttle); |
| DCHECK(throttle_registry_->GetDeferringThrottles().contains( |
| cancelling_throttle)); |
| CancelDeferredNavigationInternal(result); |
| } |
| |
| void NavigationRequest::RegisterThrottleForTesting( |
| std::unique_ptr<NavigationThrottle> navigation_throttle) { |
| // Throttles will already have run the first time the page was navigated, we |
| // won't run them again on activation. See instead CommitDeferringCondition. |
| DCHECK(!IsPageActivation()) |
| << "Attempted to register a NavigationThrottle for an activating " |
| "navigation which will not work."; |
| throttle_registry_->AddThrottle(std::move(navigation_throttle)); |
| } |
| bool NavigationRequest::IsDeferredForTesting() { |
| return IsDeferred(); |
| } |
| |
| bool NavigationRequest::IsMhtmlOrSubframe() { |
| DCHECK(state_ >= WILL_PROCESS_RESPONSE || |
| state_ == WILL_START_REQUEST && !NeedsUrlLoader()); |
| |
| return is_mhtml_or_subframe_; |
| } |
| |
| bool NavigationRequest::IsForMhtmlSubframe() const { |
| return frame_tree_node_->parent() && frame_tree_node_->frame_tree() |
| .root() |
| ->current_frame_host() |
| ->is_mhtml_document(); |
| } |
| |
| void NavigationRequest::CancelDeferredNavigationInternal( |
| NavigationThrottle::ThrottleCheckResult result) { |
| DCHECK(processing_navigation_throttle_); |
| DCHECK(result.action() == NavigationThrottle::CANCEL_AND_IGNORE || |
| result.action() == NavigationThrottle::CANCEL || |
| result.action() == NavigationThrottle::BLOCK_RESPONSE || |
| result.action() == NavigationThrottle::BLOCK_REQUEST || |
| result.action() == NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE); |
| DCHECK((result.action() != NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE && |
| result.action() != NavigationThrottle::BLOCK_REQUEST) || |
| state_ == WILL_START_REQUEST || state_ == WILL_REDIRECT_REQUEST); |
| |
| EnterChildTraceEvent("CancelDeferredNavigation", this); |
| NavigationState old_state = state_; |
| SetState(CANCELING); |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_).Run(result)) { |
| return; |
| } |
| |
| switch (old_state) { |
| case WILL_START_REQUEST: |
| OnStartChecksComplete(result); |
| return; |
| case WILL_REDIRECT_REQUEST: |
| OnRedirectChecksComplete(result); |
| return; |
| case WILL_FAIL_REQUEST: |
| OnFailureChecksComplete(result); |
| return; |
| case WILL_PROCESS_RESPONSE: |
| OnWillProcessResponseChecksComplete(result); |
| return; |
| case WILL_COMMIT_WITHOUT_URL_LOADER: |
| OnWillCommitWithoutUrlLoaderChecksComplete(result); |
| return; |
| default: |
| NOTREACHED(); |
| } |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| } |
| |
| void NavigationRequest::WillStartRequest() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::WillStartRequest", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("WillStartRequest", this); |
| DCHECK_EQ(state_, WILL_START_REQUEST); |
| will_start_request_time_ = base::TimeTicks::Now(); |
| |
| if (IsSelfReferentialURL()) { |
| SetState(CANCELING); |
| DVLOG(1) << "Cancelling self-referential request for " << GetURL(); |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_) |
| .Run(NavigationThrottle::CANCEL)) { |
| return; |
| } |
| OnWillProcessResponseChecksComplete(NavigationThrottle::CANCEL); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| return; |
| } |
| |
| // Throttles will already have run the first time the page was navigated, we |
| // won't run them again on activation. |
| if (!IsPageActivation()) { |
| base::ElapsedTimer duration; |
| throttle_registry_->RegisterNavigationThrottles(); |
| base::UmaHistogramTimes( |
| base::StrCat({"Navigation.RegisterNavigationThrottlesTime.", |
| IsInMainFrame() ? "MainFrame" : "Subframe"}), |
| duration.Elapsed()); |
| } |
| |
| // If the content/ embedder did not pass the NavigationUIData at the beginning |
| // of the navigation, ask for it now. |
| if (!navigation_ui_data_) { |
| navigation_ui_data_ = GetDelegate()->GetNavigationUIData(this); |
| } |
| |
| processing_navigation_throttle_ = true; |
| |
| base::ScopedUmaHistogramTimer timer(base::StrCat( |
| {"Navigation.ProcessNavigationThrottlesTime.WillStartRequest.", |
| IsInMainFrame() ? "MainFrame" : "SubFrame"})); |
| // Notify each throttle of the request. |
| throttle_registry_->ProcessNavigationEvent( |
| NavigationThrottleEvent::kWillStartRequest); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationHandle might have been deleted |
| // by the previous call. |
| } |
| |
| void NavigationRequest::WillRedirectRequest( |
| const GURL& new_referrer_url, |
| RenderProcessHost* post_redirect_process) { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::WillRedirectRequest", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("WillRedirectRequest", this, "url", |
| common_params_->url.possibly_invalid_spec()); |
| UpdateStateFollowingRedirect(new_referrer_url); |
| UpdateSiteInfo(post_redirect_process); |
| |
| if (IsSelfReferentialURL()) { |
| SetState(CANCELING); |
| if (complete_callback_for_testing_ && |
| std::move(complete_callback_for_testing_) |
| .Run(NavigationThrottle::CANCEL)) { |
| return; |
| } |
| OnWillProcessResponseChecksComplete(NavigationThrottle::CANCEL); |
| |
| // DO NOT ADD CODE AFTER THIS, as the NavigationRequest might have been |
| // deleted by the previous calls. |
| return; |
| } |
| |
| // Notify each throttle of the request. |
| throttle_registry_->ProcessNavigationEvent( |
| NavigationThrottleEvent::kWillRedirectRequest); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationHandle might have been deleted |
| // by the previous call. |
| } |
| |
| void NavigationRequest::WillFailRequest() { |
| EnterChildTraceEvent("WillFailRequest", this); |
| |
| SetState(WILL_FAIL_REQUEST); |
| processing_navigation_throttle_ = true; |
| |
| // Notify each throttle of the request. |
| throttle_registry_->ProcessNavigationEvent( |
| NavigationThrottleEvent::kWillFailRequest); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationHandle might have been deleted |
| // by the previous call. |
| } |
| |
| void NavigationRequest::WillProcessResponse() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::WillProcessResponse", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("WillProcessResponse", this); |
| DCHECK_EQ(state_, WILL_PROCESS_RESPONSE); |
| |
| processing_navigation_throttle_ = true; |
| was_get_response_body_called_ = false; |
| base::WeakPtr<NavigationRequest> this_ptr(weak_factory_.GetWeakPtr()); |
| |
| // Notify each throttle of the response. |
| throttle_registry_->ProcessNavigationEvent( |
| NavigationThrottleEvent::kWillProcessResponse); |
| |
| // `this` may have been deleted by the previous call. |
| if (!this_ptr) { |
| // DO NOT ADD CODE HERE. |
| return; |
| } |
| |
| CHECK(!was_get_response_body_called_ || IsDeferred()); |
| } |
| |
| void NavigationRequest::WillCommitWithoutUrlLoader() { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::WillCommitWithoutUrlLoader", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("WillCommitWithoutUrlLoader", this); |
| |
| throttle_registry_->RegisterNavigationThrottlesForCommitWithoutUrlLoader(); |
| |
| // `CommitNavigation()` expects to be called once the request has reached |
| // at least `WILL_PROCESS_REPSONSE`. `WILL_COMMIT_WITHOUT_URL_LOADER` meets |
| // that requirement, and is useful to clarify which throttles we are waiting |
| // for. |
| SetState(WILL_COMMIT_WITHOUT_URL_LOADER); |
| processing_navigation_throttle_ = true; |
| |
| throttle_registry_->ProcessNavigationEvent( |
| NavigationThrottleEvent::kWillCommitWithoutUrlLoader); |
| } |
| |
| bool NavigationRequest::IsSelfReferentialURL() { |
| // about: URLs should be exempted since they are reserved for other purposes |
| // and cannot be the source of infinite recursion. |
| // See https://crbug.com/341858 . |
| if (common_params_->url.SchemeIs(url::kAboutScheme)) |
| return false; |
| |
| // Browser-triggered navigations should be exempted. |
| if (browser_initiated()) |
| return false; |
| |
| // Some sites rely on constructing frame hierarchies where frames are loaded |
| // via POSTs with the same URLs, so exempt POST requests. |
| // See https://crbug.com/710008. |
| if (common_params_->method == "POST") |
| return false; |
| |
| // We allow one level of self-reference because some sites depend on that, |
| // but we don't allow more than one. |
| bool found_self_reference = false; |
| for (RenderFrameHost* rfh = frame_tree_node()->parent(); rfh; |
| rfh = rfh->GetParent()) { |
| if (rfh->GetLastCommittedURL().EqualsIgnoringRef(common_params_->url)) { |
| if (found_self_reference) |
| return true; |
| found_self_reference = true; |
| } |
| } |
| return false; |
| } |
| |
| void NavigationRequest::DidCommitNavigation( |
| const mojom::DidCommitProvisionalLoadParams& params, |
| bool navigation_entry_committed, |
| bool did_replace_entry, |
| const GURL& previous_main_frame_url) { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationRequest::DidCommitNavigation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| common_params_->url = params.url; |
| did_replace_entry_ = did_replace_entry; |
| should_update_history_ = params.should_update_history; |
| navigation_handle_timing_.navigation_commit_received_time = |
| params.commit_navigation_start; |
| navigation_handle_timing_.navigation_commit_reply_sent_time = |
| params.commit_reply_sent; |
| navigation_handle_timing_.navigation_did_commit_time = base::TimeTicks::Now(); |
| // A same document navigation with the same url, and no user-gesture is |
| // typically the result of 'history.replaceState().' As the page is |
| // controlling this, the user doesn't really think of this as a navigation |
| // and it doesn't make sense to log this in history. Logging this in history |
| // would lead to lots of visits to a particular page, which impacts the |
| // visit count. |
| // Navigations in non-primary frame trees don't appear in history. |
| if ((should_update_history_ && IsSameDocument() && !HasUserGesture() && |
| params.url == previous_main_frame_url) || |
| !GetRenderFrameHost()->GetPage().IsPrimary()) { |
| should_update_history_ = false; |
| } |
| previous_main_frame_url_ = previous_main_frame_url; |
| |
| // It should be kept in sync with the check in |
| // RenderFrameHostImpl::TakeNewDocumentPropertiesFromNavigation. |
| if (DidEncounterError()) { |
| EnterChildTraceEvent("DidCommitNavigation: error page", this); |
| SetState(DID_COMMIT_ERROR_PAGE); |
| } else { |
| EnterChildTraceEvent("DidCommitNavigation", this); |
| SetState(DID_COMMIT); |
| } |
| navigation_or_document_handle_->OnNavigationCommitted(*this); |
| |
| // If the navigation committed successfully, pass ownership of ViewTransition |
| // resources to the new view. This ensures that the resources are cleaned up |
| // if the new renderer process terminates before taking ownership of them. |
| if (view_transition_resources_ && state_ == DID_COMMIT) { |
| GetRenderFrameHost() |
| ->GetRenderWidgetHost() |
| ->GetRenderWidgetHostViewBase() |
| ->SetViewTransitionResources(std::move(view_transition_resources_)); |
| } |
| |
| StopCommitTimeout(); |
| |
| // Switching BrowsingInstance because of COOP or top-level cross browsing |
| // instance navigation resets the name of the frame. The renderer already |
| // knows locally about it because we sent an empty name at frame creation |
| // time. The renderer has now committed the page and we can safely enforce the |
| // empty name on the browser side. |
| BrowserContext* context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| bool should_clear_browsing_instance_name = |
| browsing_context_group_swap().ShouldClearWindowName() || |
| (commit_params().is_cross_site_cross_browsing_context_group && |
| base::FeatureList::IsEnabled( |
| features::kClearCrossSiteCrossBrowsingContextGroupWindowName) && |
| GetContentClient() |
| ->browser() |
| ->IsClearWindowNameForNewBrowsingContextGroupAllowed(context)); |
| |
| if (should_clear_browsing_instance_name) { |
| std::string name, unique_name; |
| // The "swap" only affect main frames, that have an empty unique name. |
| if (features::GetBrowsingContextMode() == |
| features::BrowsingContextStateImplementationType:: |
| kLegacyOneToOneWithFrameTreeNode) { |
| DCHECK(frame_tree_node_->unique_name().empty()); |
| GetRenderFrameHost()->browsing_context_state()->SetFrameName(name, |
| unique_name); |
| } |
| } |
| |
| // Record metrics for the time it took to commit the navigation if it was to |
| // another document without error. |
| if (!IsSameDocument() && state_ != DID_COMMIT_ERROR_PAGE) { |
| ui::PageTransition transition = |
| ui::PageTransitionFromInt(common_params_->transition); |
| base::Process::Priority priority = |
| GetRenderFrameHost()->GetProcess()->GetPriority(); |
| |
| RecordStartToCommitMetrics( |
| common_params_->navigation_start, transition, ready_to_commit_time_, |
| priority, is_same_process_, frame_tree_node_->IsMainFrame()); |
| } |
| |
| DCHECK(!frame_tree_node_->IsMainFrame() || navigation_entry_committed) |
| << "Only subframe navigations can get here without changing the " |
| << "NavigationEntry"; |
| subframe_entry_committed_ = navigation_entry_committed; |
| |
| // For successful navigations, ensure the frame owner element is no longer |
| // collapsed as a result of a prior navigation. |
| if (state_ != DID_COMMIT_ERROR_PAGE && |
| (!frame_tree_node()->IsMainFrame() || |
| frame_tree_node()->IsFencedFrameRoot())) { |
| // The last committed load in collapsed frames will be an error page with |
| // |kUnreachableWebDataURL|. Same-document navigation should not be |
| // possible. |
| DCHECK(!IsSameDocument() || !frame_tree_node()->is_collapsed()); |
| frame_tree_node()->SetCollapsed(false); |
| } |
| |
| if (service_worker_handle_ && |
| service_worker_handle_->service_worker_client()) { |
| // Notify the service worker navigation handle that the navigation finished |
| // committing. |
| service_worker_handle_->service_worker_client()->OnEndNavigationCommit(); |
| } |
| |
| // TODO(crbug.com/40249865): consider using NavigationOrDocumentHandle |
| // instead once we can get a WeakDocumentPtr from NavigationOrDocumentHandle. |
| if (subresource_proxying_url_loader_service_bind_context_) { |
| DCHECK(!IsSameDocument()); |
| |
| subresource_proxying_url_loader_service_bind_context_ |
| ->OnDidCommitNavigation(GetRenderFrameHost()->GetWeakDocumentPtr()); |
| } |
| if (keep_alive_url_loader_factory_context_) { |
| DCHECK(!IsSameDocument()); |
| |
| keep_alive_url_loader_factory_context_->OnDidCommitNavigation(this); |
| } |
| if (fetch_later_loader_factory_context_) { |
| DCHECK(!IsSameDocument()); |
| |
| fetch_later_loader_factory_context_->OnDidCommitNavigation(this); |
| } |
| |
| // Network status of the entire frame tree needs to be updated once a |
| // NavigationRequest commits. When fenced frames revoke network access by |
| // calling `window.fence.disableUntrustedNetwork`, the returned promise cannot |
| // be resolved until ongoing navigations in descendant frames complete. |
| GetRenderFrameHost() |
| ->GetOutermostMainFrame() |
| ->CalculateUntrustedNetworkStatus(); |
| |
| if (!pending_commit_metrics_.start_time.is_null()) { |
| const bool is_for_mhtml = IsMhtmlMimeType(GetMimeType()); |
| base::UmaHistogramTimes( |
| is_for_mhtml ? "Navigation.PendingCommit.Duration.MHTML" |
| : "Navigation.PendingCommit.Duration.Regular", |
| base::TimeTicks::Now() - pending_commit_metrics_.start_time); |
| const bool did_block_get_frame_host_for_navigation = |
| pending_commit_metrics_.blocked_count > 0; |
| base::UmaHistogramBoolean( |
| is_for_mhtml |
| ? "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation.MHTML" |
| : "Navigation.PendingCommit.DidBlockGetFrameHostForNavigation." |
| "Regular", |
| did_block_get_frame_host_for_navigation); |
| if (did_block_get_frame_host_for_navigation) { |
| base::UmaHistogramCounts100( |
| is_for_mhtml ? "Navigation.PendingCommit.BlockedCount.MHTML" |
| : "Navigation.PendingCommit.BlockedCount.Regular", |
| pending_commit_metrics_.blocked_count); |
| base::UmaHistogramCounts100( |
| is_for_mhtml ? "Navigation.PendingCommit.BlockedCommitCount.MHTML" |
| : "Navigation.PendingCommit.BlockedCommitCount.Regular", |
| pending_commit_metrics_.blocked_commit_count); |
| } |
| } |
| |
| // DO NOT ADD CODE after this. |
| // UnblockPendingSubframeNavigationRequestsIfNeeded() resumes throttles, which |
| // may cause the destruction of this NavigationRequest. |
| UnblockPendingSubframeNavigationRequestsIfNeeded(); |
| } |
| |
| SiteInfo NavigationRequest::GetSiteInfoForCommonParamsURL() { |
| UrlInfo url_info = GetUrlInfo(); |
| |
| // TODO(alexmos): Using |starting_site_instance_|'s IsolationContext may not |
| // be correct for cross-BrowsingInstance redirects. |
| return SiteInfo::Create(starting_site_instance_->GetIsolationContext(), |
| url_info); |
| } |
| |
| // TODO(zetamoo): Try to merge this function inside its callers. |
| void NavigationRequest::UpdateStateFollowingRedirect( |
| const GURL& new_referrer_url) { |
| // The navigation should not redirect to a "renderer debug" url. It should be |
| // blocked in NavigationRequest::OnRequestRedirected or in |
| // ResourceLoader::OnReceivedRedirect. |
| // Note: the |common_params_->url| below is the post-redirect URL. |
| // See https://crbug.com/728398. |
| CHECK(!blink::IsRendererDebugURL(common_params_->url)); |
| |
| // Re-generate the feature context to ensure that the runtime-enabled features |
| // have the correct state values. |
| runtime_feature_state_context_ = blink::RuntimeFeatureStateContext(); |
| |
| // Update the navigation parameters. |
| if (!(common_params_->transition & ui::PAGE_TRANSITION_CLIENT_REDIRECT)) { |
| sanitized_referrer_->url = new_referrer_url; |
| sanitized_referrer_ = |
| Referrer::SanitizeForRequest(common_params_->url, *sanitized_referrer_); |
| } |
| |
| common_params_->referrer = sanitized_referrer_.Clone(); |
| |
| was_redirected_ = true; |
| redirect_chain_.push_back(common_params_->url); |
| |
| SetState(WILL_REDIRECT_REQUEST); |
| processing_navigation_throttle_ = true; |
| |
| #if BUILDFLAG(IS_ANDROID) |
| navigation_handle_proxy_->DidRedirect(); |
| #endif |
| } |
| |
| void NavigationRequest::SetNavigationClient( |
| mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client) { |
| DCHECK(from_begin_navigation_ || |
| common_params_->is_history_navigation_in_new_child_frame); |
| DCHECK(!request_navigation_client_); |
| if (!navigation_client.is_valid()) |
| return; |
| |
| request_navigation_client_.reset(); |
| request_navigation_client_.Bind(std::move(navigation_client)); |
| |
| // Binds the OnAbort callback |
| HandleInterfaceDisconnection(request_navigation_client_); |
| } |
| |
| bool NavigationRequest::NeedsUrlLoader() { |
| #if BUILDFLAG(IS_ANDROID) |
| // If the navigation is for a PDF file, Chrome on Android will render it with |
| // a Java NativePage object and the navigation will always be main frame. The |
| // NativePage is responsible for reading the file and thus no URLLoader is |
| // needed. If NativePage is not enabled for PDF, |is_pdf_| should never be |
| // true. |
| if (is_pdf_) { |
| return false; |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| bool is_mhtml_subframe_loaded_from_achive = |
| IsForMhtmlSubframe() && |
| // Unlike all other MHTML subframe URLs, data-url are loaded via the |
| // URL, not from the MHTML archive. See https://crbug.com/969696. |
| !common_params_->url.SchemeIs(url::kDataScheme); |
| |
| return IsURLHandledByNetworkStack(common_params_->url) && !IsSameDocument() && |
| !is_mhtml_subframe_loaded_from_achive; |
| } |
| |
| void NavigationRequest::UpdatePrivateNetworkRequestPolicy() { |
| // It is useless to update this state for same-document navigations as well |
| // as pages served from the back-forward cache or prerendered pages. |
| DCHECK(!IsSameDocument()); |
| DCHECK(!IsPageActivation()); |
| if (GetSocketAddress().address().IsValid() && |
| GetSocketAddress().address().IsZero()) { |
| web_features_to_log_.push_back( |
| blink::mojom::WebFeature::kPrivateNetworkAccessNullIpAddress); |
| } |
| |
| ContentBrowserClient* client = GetContentClient()->browser(); |
| BrowserContext* context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| |
| url::Origin origin = GetOriginToCommit().value(); |
| ContentBrowserClient::PrivateNetworkRequestPolicyOverride policy_override = |
| client->ShouldOverridePrivateNetworkRequestPolicy(context, origin); |
| |
| if (policy_override == |
| ContentBrowserClient::PrivateNetworkRequestPolicyOverride::kForceAllow) { |
| private_network_request_policy_ = |
| network::mojom::PrivateNetworkRequestPolicy::kAllow; |
| return; |
| } |
| |
| const PolicyContainerPolicies& policies = |
| policy_container_builder_->FinalPolicies(); |
| |
| if (!policies.is_web_secure_context && |
| base::FeatureList::IsEnabled( |
| features::kBlockInsecurePrivateNetworkRequestsDeprecationTrial) && |
| // If there is no response or no headers in the response, there are |
| // definitely no trial token headers. |
| response_head_ && response_head_->headers && |
| blink::TrialTokenValidator().RequestEnablesDeprecatedFeature( |
| common_params_->url, response_head_->headers.get(), |
| "PrivateNetworkAccessNonSecureContextsAllowed", base::Time::Now())) { |
| web_features_to_log_.push_back( |
| blink::mojom::WebFeature:: |
| kPrivateNetworkAccessNonSecureContextsAllowedDeprecationTrial); |
| private_network_request_policy_ = |
| network::mojom::PrivateNetworkRequestPolicy::kAllow; |
| return; |
| } |
| |
| private_network_request_policy_ = DerivePrivateNetworkRequestPolicy( |
| policies, PrivateNetworkRequestContext::kSubresource); |
| |
| if (policy_override == |
| ContentBrowserClient::PrivateNetworkRequestPolicyOverride:: |
| kBlockInsteadOfWarn) { |
| private_network_request_policy_ = |
| OverrideBlockWithWarn(DerivePrivateNetworkRequestPolicy( |
| policies, PrivateNetworkRequestContext::kSubresource)); |
| } |
| } |
| |
| std::vector<blink::mojom::WebFeature> |
| NavigationRequest::TakeWebFeaturesToLog() { |
| std::vector<blink::mojom::WebFeature> result; |
| result.swap(web_features_to_log_); |
| return result; |
| } |
| |
| void NavigationRequest::ReadyToCommitNavigation(bool is_error) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::ReadyToCommitNavigation", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| EnterChildTraceEvent("ReadyToCommitNavigation", this); |
| |
| // We may come back to here asynchronously, and the renderer may be destroyed |
| // in the meantime. Renderer-initiated navigations listen to mojo |
| // disconnection from the renderer NavigationClient; but browser-initiated |
| // navigations do not, so we must look explicitly. We should not proceed and |
| // claim "ReadyToCommitNavigation" to the delegate if the renderer is gone. |
| if (!GetRenderFrameHost()->IsRenderFrameLive()) { |
| OnNavigationClientDisconnected(0, ""); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationHandle has been deleted |
| // by the previous call. |
| return; |
| } |
| |
| // Note: This marks the RenderFrameHost as loading. This is important to |
| // ensure that FrameTreeNode::IsLoading() still returns correct result for |
| // delegate and observer callbacks. Otherwise, there would be a period of time |
| // where the FrameTreeNode has no NavigationRequest, yet the |
| // RenderFrameHostImpl is not marked as loading yet, causing |
| // FrameTreeNode::IsLoading() to incorrectly return false. |
| frame_tree_node_->TransferNavigationRequestOwnership(GetRenderFrameHost()); |
| |
| // When a speculative RenderFrameHost reaches ReadyToCommitNavigation, the |
| // browser process has asked the renderer to commit the navigation and is |
| // waiting for confirmation of the commit. Update the LifecycleStateImpl to |
| // kPendingCommit as RenderFrameHost isn't considered speculative anymore and |
| // was chosen to commit as this navigation's final RenderFrameHost. |
| if (GetRenderFrameHost()->lifecycle_state() == |
| RenderFrameHostImpl::LifecycleStateImpl::kSpeculative) { |
| // Only cross-RenderFrameHost navigations create speculative |
| // RenderFrameHosts whereas SameDocument, BackForwardCache and |
| // PrerenderedActivation navigations don't. |
| DCHECK(!IsSameDocument() && !IsPageActivation()); |
| GetRenderFrameHost()->SetLifecycleState( |
| RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit); |
| pending_commit_metrics_.start_time = base::TimeTicks::Now(); |
| } |
| |
| // Reset the source location information, which is not needed anymore. This |
| // avoids leaking cross-origin data to another process in case the navigation |
| // doesn't commit in the same process as the document that initiated it. |
| common_params_->source_location = network::mojom::SourceLocation::New(); |
| |
| SetState(READY_TO_COMMIT); |
| ready_to_commit_time_ = base::TimeTicks::Now(); |
| RestartCommitTimeout(); |
| |
| if (!IsSameDocument() && !IsPageActivation()) |
| UpdatePrivateNetworkRequestPolicy(); |
| |
| RenderFrameHostImpl* previous_render_frame_host = |
| frame_tree_node_->current_frame_host(); |
| |
| // Record metrics for the time it takes to get to this state from the |
| // beginning of the navigation. |
| if (!IsSameDocument() && !is_error) { |
| is_same_process_ = |
| GetRenderFrameHost()->GetProcess()->GetDeprecatedID() == |
| previous_render_frame_host->GetProcess()->GetDeprecatedID(); |
| |
| RecordReadyToCommitMetrics( |
| previous_render_frame_host, GetRenderFrameHost(), *common_params_.get(), |
| ready_to_commit_time_, origin_agent_cluster_end_result_, |
| did_receive_early_hints_before_cross_origin_redirect_); |
| } |
| |
| std::optional<url::Origin> origin_to_commit = GetOriginToCommit(); |
| same_origin_ = (previous_render_frame_host->GetLastCommittedOrigin() == |
| origin_to_commit); |
| |
| SetExpectedProcess(GetRenderFrameHost()->GetProcess()); |
| |
| commit_params_->is_load_data_with_base_url = IsLoadDataWithBaseURL(); |
| commit_params_->origin_to_commit = origin_to_commit.value(); |
| |
| if (!IsSameDocument()) { |
| #if DCHECK_IS_ON() |
| DCHECK(is_safe_to_delete_); |
| base::AutoReset<bool> resetter(&is_safe_to_delete_, false); |
| #endif |
| GetDelegate()->ReadyToCommitNavigation(this); |
| } |
| |
| // View-source URLs can't be prerendered or loaded in a fenced frame. |
| if (IsInPrimaryMainFrame()) { |
| NavigationEntry* entry = GetNavigationEntry(); |
| if (entry && entry->IsViewSourceMode()) { |
| // Put the renderer in view source mode. |
| GetRenderFrameHost()->GetAssociatedLocalFrame()->EnableViewSourceMode(); |
| } |
| } |
| |
| // For fenced frames, update the mapped URL to be the URL from navigation |
| // commit (after redirects), because we want future same-origin checks to be |
| // performed with respect to the first origin committed in the fenced frame. |
| if (is_embedder_initiated_fenced_frame_navigation_) { |
| // In certain circumstances, the FencedFrameProperties will not have a |
| // mapped url. |
| // * The initial about:blank navigation in a fenced frame. |
| // In those cases, we skip this step. |
| if (fenced_frame_properties_.has_value() && |
| fenced_frame_properties_->mapped_url().has_value()) { |
| fenced_frame_properties_->UpdateMappedURL(GetURL()); |
| } |
| |
| // For fenced frames with flexible permissions, pass in information needed |
| // to build a replica of the embedder's permissions policies. This does not |
| // happen for URN iframes as they can get their embedder's permissions |
| // policies directly in the renderer. |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesLocalUnpartitionedDataAccess) && |
| GetNavigatingFrameType() == FrameType::kFencedFrameRoot && |
| fenced_frame_properties_->effective_enabled_permissions().size() == |
| 0u) { |
| fenced_frame_properties_->UpdateParentParsedPermissionsPolicy( |
| GetParentFrameOrOuterDocument()->GetPermissionsPolicy(), |
| GetParentFrameOrOuterDocument()->GetLastCommittedOrigin()); |
| } |
| } |
| |
| if (ready_to_commit_callback_for_testing_) |
| std::move(ready_to_commit_callback_for_testing_).Run(); |
| } |
| |
| bool NavigationRequest::IsWaitingToCommit() { |
| return state_ == READY_TO_COMMIT; |
| } |
| |
| bool NavigationRequest::WasResourceHintsReceived() { |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE) |
| << "Should only be called after the response started"; |
| return was_resource_hints_received_; |
| } |
| |
| bool NavigationRequest::IsPdf() { |
| return is_pdf_; |
| } |
| |
| bool NavigationRequest::IsLoadDataWithBaseURL() const { |
| // A navigation is a loadDataWithBaseURL navigation if it's a successful |
| // primary main frame navigation to a data: URL, and its base URL is valid. |
| return IsInPrimaryMainFrame() && !DidEncounterError() && |
| common_params_->url.SchemeIs(url::kDataScheme) && |
| common_params_->base_url_for_data_url.is_valid(); |
| } |
| |
| url::Origin NavigationRequest::GetTentativeOriginAtRequestTime() { |
| DCHECK_LT(state_, WILL_PROCESS_RESPONSE); |
| return GetOriginForURLLoaderFactoryBeforeResponse( |
| commit_params_->frame_policy.sandbox_flags); |
| } |
| |
| std::optional<url::Origin> NavigationRequest::GetOriginToCommit() { |
| return GetOriginForURLLoaderFactoryAfterResponse(); |
| } |
| |
| url::Origin NavigationRequest::GetOriginForURLLoaderFactoryBeforeResponse( |
| network::mojom::WebSandboxFlags sandbox_flags) { |
| // Calculate an approximation of the origin. The sandbox/csp are ignored. |
| url::Origin origin = GetOriginForURLLoaderFactoryUnchecked(); |
| |
| // Apply sandbox flags. |
| // See https://html.spec.whatwg.org/#sandboxed-origin-browsing-context-flag |
| // ``` |
| // The 'sandboxed origin browsing context flag' forces content into a unique |
| // origin, thus preventing it from accessing other content from the same |
| // origin. |
| // |
| // This flag also prevents script from reading from or writing to the |
| // document.cookie IDL attribute, and blocks access to localStorage. |
| // ``` |
| bool use_opaque_origin = |
| (sandbox_flags & network::mojom::WebSandboxFlags::kOrigin) == |
| network::mojom::WebSandboxFlags::kOrigin; |
| if (use_opaque_origin) { |
| origin = origin.DeriveNewOpaqueOrigin(); |
| } |
| |
| return origin; |
| } |
| |
| // TODO(crbug.com/40065692): Remove. |
| // Determine the relationship between the initiator and the current frame. |
| // `same`: they are the same frame. |
| // `ancestor`: the initiator is the ancestor of the navigating frame. |
| // `descendant`: the initiator is the descendant of the navigating frame. |
| // `other`: any other scenarios. |
| std::string DetermineInitiatorRelationship(RenderFrameHost* initiator_frame, |
| RenderFrameHost* current_frame) { |
| if (!current_frame || !initiator_frame) { |
| return "other"; |
| } |
| |
| if (current_frame == initiator_frame) { |
| return "same"; |
| } |
| |
| RenderFrameHost* rfh = current_frame; |
| while (rfh) { |
| rfh = rfh->GetParent(); |
| if (rfh == initiator_frame) { |
| return "ancestor"; |
| } |
| } |
| |
| rfh = initiator_frame; |
| while (rfh) { |
| rfh = rfh->GetParent(); |
| if (rfh == current_frame) { |
| return "descendant"; |
| } |
| } |
| |
| return "other"; |
| } |
| |
| std::optional<url::Origin> |
| NavigationRequest::GetOriginForURLLoaderFactoryAfterResponse() { |
| // The origin to commit is not known until we get the final network response. |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE); |
| |
| // Downloads and/or 204 responses don't commit anything - there is no frame to |
| // commit in (and therefore there is no origin that will get committed and we |
| // indicate this by returning `nullopt`). |
| if (!response_should_be_rendered_) { |
| return std::nullopt; |
| } |
| |
| if (IsSameDocument() || IsPageActivation()) { |
| CHECK(HasRenderFrameHost()); |
| return GetRenderFrameHost()->GetLastCommittedOrigin(); |
| } |
| |
| url::Origin origin = |
| GetOriginForURLLoaderFactoryBeforeResponse(SandboxFlagsToCommit()); |
| |
| SCOPED_CRASH_KEY_BOOL("Bug1454273", "is_in_main_frame", IsInMainFrame()); |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "current_origin", |
| frame_tree_node_->current_origin().GetDebugString()); |
| RenderFrameHostImpl* parent = frame_tree_node_->parent(); |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "parent_origin", |
| parent ? parent->GetLastCommittedOrigin().GetDebugString() : ""); |
| // `outer_doc` is only set when `parent` is null. |
| RenderFrameHostImpl* outer_doc = |
| parent ? nullptr : GetParentFrameOrOuterDocument(); |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "outer_doc_origin", |
| outer_doc ? outer_doc->GetLastCommittedOrigin().GetDebugString() : ""); |
| // `embedder` is only set when both `parent` and `outer_doc` are null. |
| RenderFrameHostImpl* embedder = |
| (parent || outer_doc) |
| ? nullptr |
| : frame_tree_node()->GetParentOrOuterDocumentOrEmbedder(); |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "embedder_origin", |
| embedder ? embedder->GetLastCommittedOrigin().GetDebugString() : ""); |
| |
| RenderFrameHost* initiator_rfh = GetInitiatorDocumentRenderFrameHost(); |
| SCOPED_CRASH_KEY_STRING256( |
| "Bug1454273", "initiator_origin", |
| initiator_rfh ? initiator_rfh->GetLastCommittedOrigin().GetDebugString() |
| : ""); |
| SCOPED_CRASH_KEY_STRING32( |
| "Bug1454273", "initiator_relationship", |
| DetermineInitiatorRelationship(initiator_rfh, |
| frame_tree_node_->current_frame_host())); |
| |
| // MHTML documents should commit as an opaque origin. They should not be able |
| // to make network request on behalf of the real origin. |
| // TODO(crbug.com/370979008): Migrate to CHECK. |
| DUMP_WILL_BE_CHECK(!IsMhtmlOrSubframe() || origin.opaque()); |
| |
| // If the target of this navigation will be rendered in a RenderFrameHost, |
| // then verify that the chosen origin is allowed to be accessed from that |
| // process. |
| // |
| // Note that GetRenderFrameHost() only allows retrieving the RenderFrameHost |
| // once it has been set for this navigation. This happens either at |
| // WillProcessResponse time for regular navigations or at WillFailRequest time |
| // for error pages. |
| // |
| // There are some exceptions where this check must be skipped: |
| // * Some error pages may commit in an error process that allows many origins. |
| // * MHTML iframes can load documents from any origin, no matter the current |
| // policy of the process being used. This is because the content is loaded |
| // from the MHTML archive within the process. There are no data loaded from |
| // the network. |
| if (HasRenderFrameHost() && |
| !GetRenderFrameHost()->ShouldBypassSecurityChecksForErrorPage(this) && |
| !IsForMhtmlSubframe()) { |
| int process_id = GetRenderFrameHost()->GetProcess()->GetDeprecatedID(); |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| CHECK(policy->CanAccessOrigin( |
| process_id, origin, |
| ChildProcessSecurityPolicyImpl::AccessType::kCanCommitNewOrigin)); |
| } |
| |
| return origin; |
| } |
| |
| void NavigationRequest::WriteIntoTrace( |
| perfetto::TracedProto<TraceProto> ctx) const { |
| ctx->set_navigation_id(navigation_id_); |
| ctx->set_has_committed(HasCommitted()); |
| ctx->set_is_error_page(IsErrorPage()); |
| ctx.Set(TraceProto::kFrameTreeNode, frame_tree_node_); |
| if (state_ >= WILL_PROCESS_RESPONSE) |
| ctx.Set(TraceProto::kRenderFrameHost, GetRenderFrameHost()); |
| |
| perfetto::TracedDictionary dict = std::move(ctx).AddDebugAnnotations(); |
| dict.Add("url", common_params_->url); |
| dict.Add("net_error", net_error_); |
| dict.Add("browser_initiated", commit_params_->is_browser_initiated); |
| dict.Add("from_begin_navigation", from_begin_navigation_); |
| dict.Add("is_synchronous_renderer_commit", is_synchronous_renderer_commit_); |
| dict.Add("reload_type", reload_type_); |
| dict.Add("state", state_); |
| dict.Add("navigation_type", common_params_->navigation_type); |
| |
| if (IsServedFromBackForwardCache()) { |
| dict.Add("served_from_bfcache", true); |
| dict.Add("rfh_restored_from_bfcache", |
| GetRenderFrameHostRestoredFromBackForwardCache()); |
| } |
| |
| if (prerender_frame_tree_node_id_.has_value()) { |
| dict.Add("prerender_frame_tree_node_id", |
| prerender_frame_tree_node_id_.value()); |
| } |
| } |
| |
| bool NavigationRequest::SetNavigationTimeout(base::TimeDelta timeout) { |
| // Setting a navigation-level timeout isn't implemented yet for other states, |
| // as the `loader_` must already be created. This means that callers like |
| // NavigationThrottles should only call SetNavigationTimeout from |
| // WillRedirectRequest(). If other use cases come up that need to set this |
| // timeout at other points in the NavigationRequest lifecycle, one possible |
| // solution could be to have the NavigationRequest hold the timeout value |
| // temporarily until it creates the `loader_` in OnStartChecksCompleted(). |
| DCHECK_EQ(state_, WILL_REDIRECT_REQUEST); |
| if (loader_) |
| return loader_->SetNavigationTimeout(timeout); |
| return false; |
| } |
| |
| void NavigationRequest::CancelNavigationTimeout() { |
| if (loader_) { |
| loader_->CancelNavigationTimeout(); |
| } |
| } |
| |
| void NavigationRequest::SetAllowCookiesFromBrowser( |
| bool allow_cookies_from_browser) { |
| allow_cookies_from_browser_ = allow_cookies_from_browser; |
| } |
| |
| void NavigationRequest::GetResponseBody(ResponseBodyCallback callback) { |
| CHECK_GE(state_, WILL_PROCESS_RESPONSE) |
| << "The response body should only be requested after the response body " |
| "data pipe is received from the network stack."; |
| CHECK(processing_navigation_throttle_ || IsDeferred()); |
| CHECK(response_body_callback_.is_null()); |
| CHECK(callback); |
| response_body_callback_ = std::move(callback); |
| was_get_response_body_called_ = true; |
| |
| CHECK(!response_body_watcher_); |
| response_body_watcher_ = std::make_unique<mojo::SimpleWatcher>( |
| FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL, |
| base::SequencedTaskRunner::GetCurrentDefault()); |
| response_body_watcher_->Watch( |
| response_body(), |
| MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| base::BindRepeating(&NavigationRequest::OnResponseBodyReady, |
| weak_factory_.GetWeakPtr())); |
| response_body_watcher_->ArmOrNotify(); |
| } |
| |
| void NavigationRequest::RenderProcessBlockedStateChanged(bool blocked) { |
| if (blocked) |
| StopCommitTimeout(); |
| else |
| RestartCommitTimeout(); |
| } |
| |
| void NavigationRequest::StopCommitTimeout() { |
| commit_timeout_timer_.Stop(); |
| render_process_blocked_state_changed_subscription_ = {}; |
| GetRenderFrameHost()->GetRenderWidgetHost()->RendererIsResponsive(); |
| } |
| |
| void NavigationRequest::RestartCommitTimeout() { |
| commit_timeout_timer_.Stop(); |
| if (state_ >= DID_COMMIT) |
| return; |
| |
| RenderProcessHost* renderer_host = |
| GetRenderFrameHost()->GetRenderWidgetHost()->GetProcess(); |
| if (!render_process_blocked_state_changed_subscription_) { |
| render_process_blocked_state_changed_subscription_ = |
| renderer_host->RegisterBlockStateChangedCallback(base::BindRepeating( |
| &NavigationRequest::RenderProcessBlockedStateChanged, |
| base::Unretained(this))); |
| } |
| if (!renderer_host->IsBlocked()) { |
| commit_timeout_timer_.Start( |
| FROM_HERE, g_commit_timeout, |
| base::BindOnce(&NavigationRequest::OnCommitTimeout, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void NavigationRequest::OnCommitTimeout() { |
| DCHECK_EQ(READY_TO_COMMIT, state_); |
| render_process_blocked_state_changed_subscription_ = {}; |
| |
| GetRenderFrameHost()->GetRenderWidgetHost()->RendererIsUnresponsive( |
| RenderWidgetHostImpl::RendererIsUnresponsiveReason:: |
| kNavigationRequestCommitTimeout, |
| base::BindRepeating(&NavigationRequest::RestartCommitTimeout, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // static |
| void NavigationRequest::SetCommitTimeoutForTesting( |
| const base::TimeDelta& timeout) { |
| if (timeout.is_zero()) |
| g_commit_timeout = kDefaultCommitTimeout; |
| else |
| g_commit_timeout = timeout; |
| } |
| |
| void NavigationRequest::SetPrerenderActivationNavigationState( |
| std::unique_ptr<NavigationEntryImpl> prerender_navigation_entry, |
| const blink::mojom::FrameReplicationState& replication_state) { |
| DCHECK(IsPrerenderedPageActivation()); |
| if (!prerender_navigation_state_) { |
| prerender_navigation_state_.emplace(); |
| } |
| prerender_navigation_state_->prerender_navigation_entry = |
| std::move(prerender_navigation_entry); |
| |
| // Store the replication state of the prerender main frame to copy the |
| // necessary parameters from it during activation commit and compare with the |
| // final replication state after activation to ensure it hasn't changed. |
| // TODO(crbug.com/40192974): This will need to be removed when the |
| // Browsing Instance Frame State is implemented. |
| prerender_navigation_state_->prerender_main_frame_replication_state = |
| replication_state; |
| } |
| |
| void NavigationRequest::RemoveRequestHeader(const std::string& header_name) { |
| DCHECK(state_ == WILL_REDIRECT_REQUEST); |
| removed_request_headers_.push_back(header_name); |
| } |
| |
| void NavigationRequest::SetRequestHeader(const std::string& header_name, |
| const std::string& header_value) { |
| DCHECK(state_ == WILL_START_REQUEST || state_ == WILL_REDIRECT_REQUEST); |
| modified_request_headers_.SetHeader(header_name, header_value); |
| } |
| |
| void NavigationRequest::SetCorsExemptRequestHeader( |
| const std::string& header_name, |
| const std::string& header_value) { |
| DCHECK(state_ == WILL_START_REQUEST || state_ == WILL_REDIRECT_REQUEST); |
| cors_exempt_request_headers_.SetHeader(header_name, header_value); |
| } |
| |
| void NavigationRequest::SetLCPPNavigationHint( |
| const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint& hint) { |
| DCHECK(WILL_START_REQUEST == state_ || WILL_REDIRECT_REQUEST == state_) |
| << state_; |
| commit_params_->lcpp_hint = hint.Clone(); |
| } |
| |
| const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr& |
| NavigationRequest::GetLCPPNavigationHint() { |
| return commit_params_->lcpp_hint; |
| } |
| |
| const net::HttpResponseHeaders* NavigationRequest::GetResponseHeaders() { |
| return response_head_.get() ? response_head_->headers.get() : nullptr; |
| } |
| |
| mojom::DidCommitProvisionalLoadParamsPtr |
| NavigationRequest::MakeDidCommitProvisionalLoadParamsForActivation() { |
| // Use the DidCommitProvisionalLoadParams last used to commit the frame being |
| // restored as a starting point. |
| mojom::DidCommitProvisionalLoadParamsPtr params = |
| GetRenderFrameHost()->GetPage().TakeLastCommitParams(); |
| |
| // Params must have been set when the RFH being restored from the cache last |
| // navigated. |
| CHECK(params); |
| |
| if (IsPrerenderedPageActivation()) { |
| CHECK(!prerender_navigation_utils::IsDisallowedHttpResponseCode( |
| params->http_status_code)); |
| } else { |
| DCHECK_EQ(params->http_status_code, net::HTTP_OK); |
| } |
| DCHECK_EQ(params->url_is_unreachable, false); |
| |
| DCHECK_EQ(params->post_id, -1); |
| params->navigation_token = commit_params().navigation_token; |
| DCHECK_EQ(params->url, common_params().url); |
| params->should_update_history = true; |
| DCHECK_EQ(params->method, common_params().method); |
| params->item_sequence_number = frame_entry_item_sequence_number_; |
| params->document_sequence_number = frame_entry_document_sequence_number_; |
| params->transition = ui::PageTransitionFromInt(common_params().transition); |
| params->history_list_was_cleared = false; |
| params->request_id = GetGlobalRequestID().request_id; |
| |
| return params; |
| } |
| |
| mojom::DidCommitProvisionalLoadParamsPtr |
| NavigationRequest::MakeDidCommitProvisionalLoadParamsForBFCacheRestore() { |
| // Start with the provisional load parameters shared between all page |
| // activation types. |
| mojom::DidCommitProvisionalLoadParamsPtr params = |
| MakeDidCommitProvisionalLoadParamsForActivation(); |
| |
| // Add bfcache-specific provisional load params: |
| params->did_create_new_entry = false; |
| params->page_state = |
| blink::PageState::CreateFromEncodedData(commit_params().page_state); |
| return params; |
| } |
| |
| mojom::DidCommitProvisionalLoadParamsPtr |
| NavigationRequest::MakeDidCommitProvisionalLoadParamsForPrerenderActivation() { |
| DCHECK(IsPrerenderedPageActivation()); |
| |
| // Start with the provisional load parameters shared between all page |
| // activation types. |
| mojom::DidCommitProvisionalLoadParamsPtr params = |
| MakeDidCommitProvisionalLoadParamsForActivation(); |
| // TODO(crbug.com/40169536): Investigate when a new entry should |
| // replace an old one when prerendering a page. |
| params->did_create_new_entry = true; |
| // Prerendering already has a navigation entry which has correct PageState. |
| // Set params->page_state accordingly to ensure that DCHECKs expecting them to |
| // match are happy. |
| // Note: |params| are using last commit params as a basis (via |
| // TakeLastCommitParams call), which have a page state from the last commit, |
| // but the page state might have been updated since the last commit. |
| params->page_state = prerender_navigation_state_->prerender_navigation_entry |
| ->GetFrameEntry(frame_tree_node()) |
| ->page_state(); |
| |
| // insecure_request_policy field of the replication state is set during the |
| // navigation commit based on DidCommitProvisionalLoadParams. As prerendering |
| // activates existing page, copy its main frame replication state to ensure |
| // that the effective replication state doesn't change after activation. |
| // TODO(crbug.com/40192974): replication state should belong to the Browsing |
| // Instance Frame State. |
| params->insecure_request_policy = |
| prerender_navigation_state_->prerender_main_frame_replication_state |
| .insecure_request_policy; |
| return params; |
| } |
| |
| bool NavigationRequest::IsExternalProtocol() { |
| return !GetContentClient()->browser()->IsHandledURL(common_params_->url); |
| } |
| |
| bool NavigationRequest::IsSignedExchangeInnerResponse() { |
| return response() && response()->is_signed_exchange_inner_response; |
| } |
| |
| net::IPEndPoint NavigationRequest::GetSocketAddress() { |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE); |
| return response() ? response()->remote_endpoint : net::IPEndPoint(); |
| } |
| |
| bool NavigationRequest::HasCommitted() const { |
| return state_ == DID_COMMIT || state_ == DID_COMMIT_ERROR_PAGE; |
| } |
| |
| bool NavigationRequest::IsErrorPage() const { |
| return state_ == DID_COMMIT_ERROR_PAGE; |
| } |
| |
| bool NavigationRequest::DidEncounterError() const { |
| return net_error_ != net::OK; |
| } |
| |
| net::HttpConnectionInfo NavigationRequest::GetConnectionInfo() { |
| return response() ? response()->connection_info : net::HttpConnectionInfo(); |
| } |
| |
| bool NavigationRequest::IsInMainFrame() const { |
| return frame_tree_node()->IsMainFrame(); |
| } |
| |
| RenderFrameHostImpl* NavigationRequest::GetParentFrame() { |
| return IsInMainFrame() ? nullptr : frame_tree_node()->parent(); |
| } |
| |
| RenderFrameHostImpl* NavigationRequest::GetParentFrameOrOuterDocument() { |
| return frame_tree_node()->GetParentOrOuterDocument(); |
| } |
| |
| bool NavigationRequest::IsInPrimaryMainFrame() const { |
| return GetNavigatingFrameType() == FrameType::kPrimaryMainFrame; |
| } |
| |
| bool NavigationRequest::IsInOutermostMainFrame() const { |
| switch (GetNavigatingFrameType()) { |
| case FrameType::kPrimaryMainFrame: |
| case FrameType::kPrerenderMainFrame: |
| case FrameType::kGuestMainFrame: |
| return true; |
| case FrameType::kSubframe: |
| case FrameType::kFencedFrameRoot: |
| return false; |
| } |
| } |
| |
| bool NavigationRequest::IsInPrerenderedMainFrame() const { |
| return GetNavigatingFrameType() == FrameType::kPrerenderMainFrame; |
| } |
| |
| bool NavigationRequest::IsPrerenderedPageActivation() const { |
| CHECK(prerender_frame_tree_node_id_.has_value()); |
| return !prerender_frame_tree_node_id_.value().is_null(); |
| } |
| |
| bool NavigationRequest::IsInFencedFrameTree() const { |
| return frame_tree_node()->IsInFencedFrameTree(); |
| } |
| |
| bool NavigationRequest::IsGuestViewMainFrame() const { |
| return GetNavigatingFrameType() == content::FrameType::kGuestMainFrame; |
| } |
| |
| FrameType NavigationRequest::GetNavigatingFrameType() const { |
| return frame_tree_node()->GetFrameType(); |
| } |
| |
| FrameTreeNodeId NavigationRequest::GetFrameTreeNodeId() { |
| return frame_tree_node()->frame_tree_node_id(); |
| } |
| |
| bool NavigationRequest::WasResponseCached() { |
| return response() && response()->was_fetched_via_cache; |
| } |
| |
| bool NavigationRequest::HasPrefetchedAlternativeSubresourceSignedExchange() { |
| return !commit_params_->prefetched_signed_exchanges.empty(); |
| } |
| |
| int64_t NavigationRequest::GetNavigationId() const { |
| return navigation_id_; |
| } |
| |
| ukm::SourceId NavigationRequest::GetNextPageUkmSourceId() { |
| // If the navigation is restoring from back-forward cache, the UKM id |
| // will get restored, too. |
| if (IsServedFromBackForwardCache()) { |
| return GetRenderFrameHostRestoredFromBackForwardCache() |
| ->GetPageUkmSourceId(); |
| } |
| |
| // If this is the same document or a subframe navigation (i.e. iframe or |
| // fenced frame), the UKM id will not change from it. |
| if (IsSameDocument() || !IsInMainFrame() || IsInFencedFrameTree()) |
| return previous_page_ukm_source_id_; |
| |
| return ukm::ConvertToSourceId(navigation_id_, |
| ukm::SourceIdObj::Type::NAVIGATION_ID); |
| } |
| |
| const GURL& NavigationRequest::GetURL() { |
| return common_params().url; |
| } |
| |
| SiteInstanceImpl* NavigationRequest::GetStartingSiteInstance() { |
| return starting_site_instance_.get(); |
| } |
| |
| SiteInstanceImpl* NavigationRequest::GetSourceSiteInstance() { |
| return source_site_instance_.get(); |
| } |
| |
| bool NavigationRequest::IsRendererInitiated() { |
| return !commit_params_->is_browser_initiated; |
| } |
| |
| blink::mojom::NavigationInitiatorActivationAndAdStatus |
| NavigationRequest::GetNavigationInitiatorActivationAndAdStatus() { |
| return begin_params_->initiator_activation_and_ad_status; |
| } |
| |
| bool NavigationRequest::IsSameOrigin() { |
| DCHECK(HasCommitted()); |
| return same_origin_; |
| } |
| |
| bool NavigationRequest::WasServerRedirect() { |
| return was_redirected_; |
| } |
| |
| const std::vector<GURL>& NavigationRequest::GetRedirectChain() { |
| return redirect_chain_; |
| } |
| |
| base::TimeTicks NavigationRequest::NavigationStart() { |
| return common_params().navigation_start; |
| } |
| |
| base::TimeTicks NavigationRequest::NavigationInputStart() { |
| return common_params().input_start; |
| } |
| |
| const NavigationHandleTiming& NavigationRequest::GetNavigationHandleTiming() { |
| return navigation_handle_timing_; |
| } |
| |
| bool NavigationRequest::IsPost() { |
| return common_params().method == "POST"; |
| } |
| |
| std::string NavigationRequest::GetRequestMethod() { |
| return request_method_; |
| } |
| |
| const blink::mojom::Referrer& NavigationRequest::GetReferrer() { |
| return *sanitized_referrer_; |
| } |
| |
| void NavigationRequest::SetReferrer(blink::mojom::ReferrerPtr referrer) { |
| DCHECK(state_ == WILL_START_REQUEST || state_ == WILL_REDIRECT_REQUEST); |
| sanitized_referrer_ = |
| Referrer::SanitizeForRequest(common_params_->url, *referrer); |
| common_params_->referrer = sanitized_referrer_.Clone(); |
| } |
| |
| bool NavigationRequest::HasUserGesture() { |
| return common_params().has_user_gesture; |
| } |
| |
| ui::PageTransition NavigationRequest::GetPageTransition() { |
| return ui::PageTransitionFromInt(common_params().transition); |
| } |
| |
| NavigationUIData* NavigationRequest::GetNavigationUIData() { |
| return navigation_ui_data_.get(); |
| } |
| |
| net::Error NavigationRequest::GetNetErrorCode() { |
| return net_error_; |
| } |
| |
| int NavigationRequest::GetNetExtendedErrorCode() { |
| return extended_error_code_; |
| } |
| |
| // The RenderFrameHost that will commit the navigation or an error page. |
| // This is computed when the response is received, or when the navigation |
| // fails and error page should be displayed. |
| RenderFrameHostImpl* NavigationRequest::GetRenderFrameHost() const { |
| // Only allow the RenderFrameHost to be retrieved once it has been set for |
| // this navigation. This will happens either at WillProcessResponse time for |
| // regular navigations or at WillFailRequest time for error pages. |
| // NavigationRequests created for synchronous renderer commits (see |
| // documentation for |is_synchronous_renderer_commit_|) have a |
| // RenderFrameHost available from the start. |
| if (!is_synchronous_renderer_commit()) { |
| CHECK_GE(state_, WILL_PROCESS_RESPONSE) |
| << "This accessor should only be called after a RenderFrameHost has " |
| "been picked for this navigation."; |
| } |
| static_assert(WILL_FAIL_REQUEST > WILL_PROCESS_RESPONSE, |
| "WillFailRequest state should come after WillProcessResponse"); |
| if (HasRenderFrameHost()) { |
| return &*render_frame_host_.value(); |
| } |
| return nullptr; |
| } |
| |
| NavigationRequest::AssociatedRenderFrameHostType |
| NavigationRequest::GetAssociatedRFHType() const { |
| // `associated_rfh_type_` might not be accurate after the navigation had |
| // moved to the RFH. This is because if another navigation had committed and |
| // committed a new RFH that replaces the current RFH, the |
| // `associated_rfh_type_` might be stale and needs to be updated. However, |
| // we only update the value for non-pending commit navigations (i.e. the |
| // NavigationRequest owned by the FrameTreeNode). See the comments in |
| // `RenderFrameHostManager::CommitPendingIfNecessary()` for more details. |
| CHECK(state_ < READY_TO_COMMIT || state_ == WILL_FAIL_REQUEST) |
| << "Use GetRenderFrameHost() instead when the final RenderFrameHost " |
| "for the navigation has been picked"; |
| return associated_rfh_type_; |
| } |
| |
| void NavigationRequest::SetAssociatedRFHType( |
| AssociatedRenderFrameHostType type) { |
| if (associated_rfh_type_ != AssociatedRenderFrameHostType::NONE && |
| type == AssociatedRenderFrameHostType::NONE) { |
| // If we're transitioning to "NONE" when the previous state was not "NONE", |
| // we might have called SetExpectedProcess() before, so reset it now. |
| ResetExpectedProcess(); |
| } |
| associated_rfh_type_ = type; |
| } |
| |
| const net::HttpRequestHeaders& NavigationRequest::GetRequestHeaders() { |
| if (!request_headers_) { |
| request_headers_.emplace(); |
| request_headers_->AddHeadersFromString(begin_params_->headers); |
| } |
| return *request_headers_; |
| } |
| |
| const std::optional<net::SSLInfo>& NavigationRequest::GetSSLInfo() { |
| return ssl_info_; |
| } |
| |
| const std::optional<net::AuthChallengeInfo>& |
| NavigationRequest::GetAuthChallengeInfo() { |
| return auth_challenge_info_; |
| } |
| |
| net::ResolveErrorInfo NavigationRequest::GetResolveErrorInfo() { |
| return resolve_error_info_; |
| } |
| |
| net::IsolationInfo NavigationRequest::GetIsolationInfo() { |
| if (isolation_info_) |
| return isolation_info_.value(); |
| |
| // TODO(crbug.com/40634002): Consider changing this code to copy an origin |
| // instead of creating one from a URL which lacks opacity information. |
| return frame_tree_node_->current_frame_host() |
| ->ComputeIsolationInfoForNavigation( |
| common_params_->url, is_credentialless(), ComputeFencedFrameNonce()); |
| } |
| |
| bool NavigationRequest::HasSubframeNavigationEntryCommitted() { |
| DCHECK(!frame_tree_node_->IsMainFrame()); |
| DCHECK(state_ == DID_COMMIT || state_ == DID_COMMIT_ERROR_PAGE); |
| return subframe_entry_committed_; |
| } |
| |
| bool NavigationRequest::DidReplaceEntry() { |
| DCHECK(state_ == DID_COMMIT || state_ == DID_COMMIT_ERROR_PAGE); |
| return did_replace_entry_; |
| } |
| |
| bool NavigationRequest::ShouldUpdateHistory() { |
| DCHECK(state_ == DID_COMMIT || state_ == DID_COMMIT_ERROR_PAGE); |
| return should_update_history_; |
| } |
| |
| const GURL& NavigationRequest::GetPreviousPrimaryMainFrameURL() { |
| DCHECK(IsInPrimaryMainFrame() || |
| GetParentFrame() && GetParentFrame()->GetPage().IsPrimary()); |
| return GetPreviousMainFrameURL(); |
| } |
| |
| const GURL& NavigationRequest::GetPreviousMainFrameURL() const { |
| DCHECK(state_ == DID_COMMIT || state_ == DID_COMMIT_ERROR_PAGE); |
| return previous_main_frame_url_; |
| } |
| |
| bool NavigationRequest::WasStartedFromContextMenu() { |
| return common_params().started_from_context_menu; |
| } |
| |
| const GURL& NavigationRequest::GetSearchableFormURL() { |
| return begin_params().searchable_form_url; |
| } |
| |
| const std::string& NavigationRequest::GetSearchableFormEncoding() { |
| return begin_params().searchable_form_encoding; |
| } |
| |
| ReloadType NavigationRequest::GetReloadType() const { |
| return reload_type_; |
| } |
| |
| RestoreType NavigationRequest::GetRestoreType() const { |
| return restore_type_; |
| } |
| |
| const GURL& NavigationRequest::GetBaseURLForDataURL() { |
| return common_params().base_url_for_data_url; |
| } |
| |
| const GlobalRequestID& NavigationRequest::GetGlobalRequestID() { |
| DCHECK_GE(state_, WILL_PROCESS_RESPONSE); |
| return request_id_; |
| } |
| |
| bool NavigationRequest::IsDownload() { |
| return is_download_; |
| } |
| |
| bool NavigationRequest::IsFormSubmission() { |
| return begin_params().is_form_submission; |
| } |
| |
| bool NavigationRequest::WasInitiatedByLinkClick() { |
| return begin_params().was_initiated_by_link_click; |
| } |
| |
| const std::string& NavigationRequest::GetHrefTranslate() { |
| return common_params().href_translate; |
| } |
| |
| const std::optional<blink::Impression>& NavigationRequest::GetImpression() { |
| return begin_params().impression; |
| } |
| |
| const std::optional<blink::LocalFrameToken>& |
| NavigationRequest::GetInitiatorFrameToken() { |
| return initiator_frame_token_; |
| } |
| |
| int NavigationRequest::GetInitiatorProcessId() { |
| return initiator_process_id_; |
| } |
| |
| const std::optional<url::Origin>& NavigationRequest::GetInitiatorOrigin() { |
| return common_params().initiator_origin; |
| } |
| |
| const std::optional<GURL>& NavigationRequest::GetInitiatorBaseUrl() { |
| return common_params().initiator_base_url; |
| } |
| |
| const std::vector<std::string>& NavigationRequest::GetDnsAliases() { |
| static const base::NoDestructor<std::vector<std::string>> emptyvector_result; |
| return response_head_ ? response_head_->dns_aliases : *emptyvector_result; |
| } |
| |
| bool NavigationRequest::IsSameProcess() { |
| return is_same_process_; |
| } |
| |
| NavigationEntry* NavigationRequest::GetNavigationEntry() const { |
| if (nav_entry_id_ == 0) |
| return nullptr; |
| |
| return GetNavigationController()->GetEntryWithUniqueIDIncludingPending( |
| nav_entry_id_); |
| } |
| |
| int NavigationRequest::GetNavigationEntryOffset() const { |
| return navigation_entry_offset_; |
| } |
| |
| GlobalRenderFrameHostId NavigationRequest::GetPreviousRenderFrameHostId() { |
| if (previous_render_frame_host_id_ != GlobalRenderFrameHostId()) { |
| CHECK_GE(state_, READY_TO_COMMIT); |
| // If `previous_render_frame_host_id_` is set to a non-default value, then |
| // the navigation had committed and potentially replaced the previous |
| // "current RenderFrameHost", so we return the saved value of that previous |
| // RenderFrameHost's ID here. |
| return previous_render_frame_host_id_; |
| } |
| |
| // The navigation hasn't committed yet, so the previous RenderFrameHost is |
| // still the current RenderFrameHost. Note that this might be different from |
| // the current RFH at NavigationRequest construction time (whose FTN id value |
| // is saved in `current_render_frame_host_id_at_construction_`), if another |
| // navigation caused a new RenderFrameHost to be committed while this |
| // navigation is in progress. |
| if (frame_tree_node_->current_frame_host()) { |
| return frame_tree_node_->current_frame_host()->GetGlobalId(); |
| } else { |
| // It's possible for `frame_tree_node_->current_frame_host()` to be null if |
| // we're in the middle of destructing the navigating FrameTreeNode. In this |
| // case, just return `current_render_frame_host_id_at_construction_`. |
| return current_render_frame_host_id_at_construction_; |
| } |
| } |
| |
| ChildProcessId NavigationRequest::GetExpectedRenderProcessHostId() { |
| DCHECK_LT(state_, READY_TO_COMMIT); |
| return expected_render_process_host_id_; |
| } |
| |
| bool NavigationRequest::IsServedFromBackForwardCache() { |
| const NavigationRequest& request = *this; |
| return request.IsServedFromBackForwardCache(); |
| } |
| |
| void NavigationRequest::SetIsOverridingUserAgent(bool override_ua) { |
| // Only add specific headers when creating a NavigationRequest before the |
| // network request is made, not at commit time. |
| if (is_synchronous_renderer_commit_) |
| return; |
| |
| // This code assumes it is only called from DidStartNavigation(). |
| DCHECK(!ua_change_requires_reload_); |
| |
| commit_params_->is_overriding_user_agent = override_ua; |
| // The new document, created by this navigation, will be honoring the new |
| // value. It will be reflected into its NavigationEntry's when committing the |
| // new document at DidCommitNavigation time. |
| |
| net::HttpRequestHeaders headers; |
| headers.AddHeadersFromString(begin_params_->headers); |
| BrowserContext* browser_context = |
| frame_tree_node_->navigator().controller().GetBrowserContext(); |
| ClientHintsControllerDelegate* client_hints_delegate = |
| browser_context->GetClientHintsControllerDelegate(); |
| if (client_hints_delegate) { |
| UpdateNavigationRequestClientUaHeaders( |
| GetTentativeOriginAtRequestTime(), client_hints_delegate, |
| is_overriding_user_agent(), frame_tree_node_, &headers, |
| common_params_->url); |
| } |
| headers.SetHeader( |
| net::HttpRequestHeaders::kUserAgent, |
| ComputeUserAgentValue(headers, GetUserAgentOverride(), browser_context)); |
| begin_params_->headers = headers.ToString(); |
| // |request_headers_| comes from |begin_params_|. Clear |request_headers_| now |
| // so that if |request_headers_| are needed, they will be updated. |
| request_headers_.reset(); |
| } |
| |
| void NavigationRequest::SetSilentlyIgnoreErrors() { |
| silently_ignore_errors_ = true; |
| } |
| |
| void NavigationRequest::SetVisitedLinkSalt(uint64_t salt) { |
| commit_params_->visited_link_salt = salt; |
| } |
| |
| // static |
| NavigationRequest* NavigationRequest::From(NavigationHandle* handle) { |
| return static_cast<NavigationRequest*>(handle); |
| } |
| |
| // static |
| ReloadType NavigationRequest::NavigationTypeToReloadType( |
| blink::mojom::NavigationType type) { |
| if (type == blink::mojom::NavigationType::RELOAD) |
| return ReloadType::NORMAL; |
| if (type == blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE) |
| return ReloadType::BYPASSING_CACHE; |
| return ReloadType::NONE; |
| } |
| |
| bool NavigationRequest::IsNavigationStarted() const { |
| return is_navigation_started_; |
| } |
| |
| bool NavigationRequest::RequiresInitiatorBasedSourceSiteInstance() const { |
| // Browser-initiated navigations can supply an initiator origin without having |
| // an associated source SiteInstance (e.g. Android intents handled by Chrome). |
| // However, the context menu is a case in which we should have one, so it |
| // should still require a source SiteInstance. |
| if (commit_params_->is_browser_initiated && |
| !common_params().started_from_context_menu) { |
| return false; |
| } |
| |
| // data: URLs, about:blank and empty URL (which are treated the same as |
| // about:blank) navigations that have initiator origins require a source |
| // SiteInstance. |
| const bool is_data_or_about_or_empty = |
| common_params_->url.SchemeIs(url::kDataScheme) || |
| common_params_->url.IsAboutBlank() || common_params_->url.is_empty(); |
| |
| const bool has_valid_initiator = |
| common_params_->initiator_origin && |
| common_params_->initiator_origin->GetTupleOrPrecursorTupleIfOpaque() |
| .IsValid(); |
| |
| return is_data_or_about_or_empty && has_valid_initiator && |
| !dest_site_instance_; |
| } |
| |
| void NavigationRequest::SetSourceSiteInstanceToInitiatorIfNeeded() { |
| if (source_site_instance_ || !RequiresInitiatorBasedSourceSiteInstance()) |
| return; |
| |
| // TODO(crbug.com/349972037): Reconsider source SiteInstance usage. E.g. if |
| // the initiator is sandboxed, the source SiteInstance we get here isn't and |
| // they don't technically match. |
| const auto tuple = |
| common_params_->initiator_origin->GetTupleOrPrecursorTupleIfOpaque(); |
| source_site_instance_ = static_cast<SiteInstanceImpl*>( |
| frame_tree_node_->current_frame_host() |
| ->GetSiteInstance() |
| ->GetRelatedSiteInstance(tuple.GetURL()) |
| .get()); |
| } |
| |
| void NavigationRequest::ForceEnableOriginTrials( |
| const std::vector<std::string>& trials) { |
| DCHECK(!HasCommitted()); |
| commit_params_->force_enabled_origin_trials = trials; |
| } |
| |
| network::CrossOriginEmbedderPolicy |
| NavigationRequest::ComputeCrossOriginEmbedderPolicy() { |
| const auto& url = common_params_->url; |
| // Fenced Frames should respect the outer frame's COEP. |
| RenderFrameHostImpl* const parent = GetParentFrameOrOuterDocument(); |
| bool is_fenced_frame_from_local_scheme = |
| GetNavigatingFrameType() == FrameType::kFencedFrameRoot && |
| (url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme)); |
| |
| // Some special URLs not loaded using the network inherit the |
| // Cross-Origin-Embedder-Policy header from their parent. |
| if (parent && (GetContentClient() |
| ->browser() |
| ->ShouldInheritCrossOriginEmbedderPolicyImplicitly(url) || |
| is_fenced_frame_from_local_scheme)) { |
| return parent->cross_origin_embedder_policy(); |
| } |
| |
| // Compute "topLevelCreationURL" for COEP and secure context. |
| // |
| // [spec]: https://html.spec.whatwg.org/C/#initialise-the-document-object |
| // 3. Let creationURL be navigationParams's response's URL. |
| // 5. If browsingContext is still on its initial about:blank Document [...] |
| // 6. Otherwise: |
| // 6.6. Let topLevelCreationURL be creationURL. |
| // 6.8. If browsingContext is not a top-level browsing context, then: |
| // 6.8.2. Set topLevelCreationURL to parentEnvironment's top-level creation |
| // URL. |
| // |
| // TODO(arthursonzogni): It would be good to clarify what |
| // |topLevelCreationURL| means when loading FencedFrame//GuestView, and how it |
| // should affect COEP. |
| // Tracking bug: |
| // - FencedFrame: https://crbug.com/1277430 |
| // - GuestView: XXX or slightly related https://crbug.com/1260747 |
| const GURL& top_level_creation_url = GetParentFrameOrOuterDocument() |
| ? GetParentFrameOrOuterDocument() |
| ->GetOutermostMainFrame() |
| ->GetLastCommittedURL() |
| : url; |
| // [spec]: https://html.spec.whatwg.org/C/#obtain-an-embedder-policy |
| // |
| // 1. Let policy be a new embedder policy. |
| // 2. If environment is a non-secure context, then return policy. |
| if (network::IsUrlPotentiallyTrustworthy(top_level_creation_url)) { |
| if (response_head_) { |
| return response_head_->parsed_headers->cross_origin_embedder_policy; |
| } |
| } |
| return network::CrossOriginEmbedderPolicy(); |
| } |
| |
| NavigationRequest::IntegrityPolicies |
| NavigationRequest::ComputeIntegrityPolicies() { |
| IntegrityPolicies policies; |
| if (!base::FeatureList::IsEnabled( |
| network::features::kIntegrityPolicyScript)) { |
| return policies; |
| } |
| if (response_head_ && response_head_->parsed_headers) { |
| policies.enforced = response_head_->parsed_headers->integrity_policy; |
| policies.report_only = |
| response_head_->parsed_headers->integrity_policy_report_only; |
| } |
| return policies; |
| } |
| |
| // [spec]: |
| // https://html.spec.whatwg.org/C/#check-a-navigation-response's-adherence-to-its-embedder-policy |
| // |
| // Return whether the child's |coep| is compatible with its parent's COEP. It |
| // also sends COEP reports if needed. |
| bool NavigationRequest::CheckResponseAdherenceToCoep(const GURL& url) { |
| const auto& coep = |
| policy_container_builder_->FinalPolicies().cross_origin_embedder_policy; |
| |
| // Fenced Frames should respect the outer frame's COEP. |
| RenderFrameHostImpl* const parent = GetParentFrameOrOuterDocument(); |
| |
| // [spec]: 1. If target is not a child browsing context, then return true. |
| if (!parent) |
| return true; |
| |
| // [spec]: 2. Let parentPolicy be target's container document's policy |
| // container's embedder policy. |
| const auto& parent_coep = parent->cross_origin_embedder_policy(); |
| CrossOriginEmbedderPolicyReporter* parent_coep_reporter = |
| parent->coep_reporter(); |
| |
| // [spec]: 3. If parentPolicy's report-only value is compatible with |
| // cross-origin isolation and responsePolicy's value is not, then queue a |
| // cross-origin embedder policy inheritance violation [...]. |
| if (CoepBlockIframe(parent_coep.report_only_value, coep.value, |
| is_credentialless())) { |
| if (parent_coep_reporter) { |
| parent_coep_reporter->QueueNavigationReport(redirect_chain_[0], |
| /*report_only=*/true); |
| } |
| } |
| |
| // [spec]: 4. If parentPolicy's value is not compatible with cross-origin |
| // isolation or responsePolicy's value is compatible with cross-origin |
| // isolation, then return true. |
| if (!CoepBlockIframe(parent_coep.value, coep.value, is_credentialless())) |
| return true; |
| |
| // [spec]: 5 Queue a cross-origin embedder policy inheritance violation with |
| // response, "navigation", parentPolicy's reporting endpoint, "enforce", and |
| // target's container document's relevant settings object. |
| if (parent_coep_reporter) { |
| parent_coep_reporter->QueueNavigationReport(redirect_chain_[0], |
| /*report_only=*/false); |
| } |
| |
| // [spec]: 6. Return false. |
| return false; |
| } |
| |
| std::optional<network::mojom::BlockedByResponseReason> |
| NavigationRequest::EnforceCOEP() { |
| // https://html.spec.whatwg.org/C/#check-a-navigation-response's-adherence-to-its-embedder-policy |
| // Spec should be updated: |
| // https://github.com/shivanigithub/fenced-frame/issues/11 |
| |
| // Fenced frames should be treated as an embedded frame, thus COEP must apply. |
| RenderFrameHostImpl* const parent_frame = GetParentFrameOrOuterDocument(); |
| if (!parent_frame) { |
| return std::nullopt; |
| } |
| if (is_credentialless()) { |
| return std::nullopt; |
| } |
| const auto& url = common_params_->url; |
| // Some special URLs not loaded using the network are inheriting the |
| // Cross-Origin-Embedder-Policy header from their parent. |
| if (url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme)) { |
| return std::nullopt; |
| } |
| return network::CrossOriginResourcePolicy::IsNavigationBlocked( |
| url, redirect_chain_[0], parent_frame->GetLastCommittedOrigin(), |
| *response_head_, request_destination(), |
| parent_frame->cross_origin_embedder_policy(), |
| parent_frame->coep_reporter()); |
| } |
| |
| void NavigationRequest::CoopCoepSanityCheck() { |
| // Same-document navigations simply reuse the current document and do not use |
| // the PolicyContainer, which may contain erroneous information. For example |
| // in pushState/popState history navigations. See https://crbug.com/1413081. |
| if (IsSameDocument()) { |
| return; |
| } |
| |
| // Credentialless iframes allow frames to be crossOriginIsolated without ever |
| // setting COEP, so the below check does not apply. |
| if (is_credentialless_) { |
| return; |
| } |
| |
| const PolicyContainerPolicies& policies = |
| policy_container_builder_->FinalPolicies(); |
| // Use GetParentFrameOrOuterDocument() to respect the outer frame's COEP for |
| // now. |
| network::mojom::CrossOriginOpenerPolicyValue coop_value = |
| GetParentFrameOrOuterDocument() |
| ? GetRenderFrameHost() |
| ->GetOutermostMainFrame() |
| ->cross_origin_opener_policy() |
| .value |
| : policies.cross_origin_opener_policy.value; |
| |
| if (coop_value == |
| network::mojom::CrossOriginOpenerPolicyValue::kSameOriginPlusCoep && |
| !CompatibleWithCrossOriginIsolated( |
| policies.cross_origin_embedder_policy)) { |
| NOTREACHED(); |
| } |
| } |
| |
| bool NavigationRequest::IsFencedFrameRequiredPolicyFeatureAllowed( |
| const url::Origin& origin, |
| const network::mojom::PermissionsPolicyFeature feature) { |
| const network::PermissionsPolicyFeatureList& feature_list = |
| network::GetPermissionsPolicyFeatureList(origin); |
| |
| // Check if the outer document's permissions policies allow all of the |
| // required policies. |
| std::optional<const network::PermissionsPolicy::Allowlist> |
| embedder_allowlist = GetParentFrameOrOuterDocument() |
| ->GetPermissionsPolicy() |
| ->GetAllowlistForFeatureIfExists(feature); |
| if (embedder_allowlist && !embedder_allowlist->MatchesAll()) { |
| return false; |
| } |
| |
| // Check if the container policies to be committed allow all of the required |
| // policies for `origin`. This means that the policy must be either |
| // explicitly enabled for `origin`, or the policy must by default |
| // be enabled for all origins. Note: because the policies have not been |
| // read into a RenderFrameHost's permissions_policy_ yet, we need to check |
| // the network::ParsedPermissionsPolicyDeclaration object directly. |
| auto policy_iter = std::find_if( |
| commit_params_->frame_policy.container_policy.begin(), |
| commit_params_->frame_policy.container_policy.end(), |
| [feature](const network::ParsedPermissionsPolicyDeclaration& d) { |
| return d.feature == feature; |
| }); |
| if (policy_iter == commit_params_->frame_policy.container_policy.end()) { |
| return feature_list.at(feature) == |
| network::PermissionsPolicyFeatureDefault::EnableForAll; |
| } |
| |
| return policy_iter->Contains(origin); |
| } |
| |
| bool NavigationRequest::CheckPermissionsPoliciesForFencedFrames( |
| const url::Origin& origin) { |
| // These checks only apply to fenced frames. |
| if (!frame_tree_node_->IsFencedFrameRoot()) |
| return true; |
| |
| const std::optional<FencedFrameProperties>& computed_fenced_frame_properties = |
| ComputeFencedFrameProperties(); |
| |
| // Permissions policies only need to be checked for fenced frames created from |
| // an API like FLEDGE or Shared Storage. |
| if (!computed_fenced_frame_properties) { |
| return true; |
| } |
| |
| // Check that all of the required policies for a new document with origin |
| // `origin` in the fenced frame are allowed. This looks at the outer |
| // document's policies and the "allow" attribute. Note that the document will |
| // eventually only use the required policies without policy inheritance, so |
| // extra policies defined in the outer document/"allow" attribute won't have |
| // any effect. |
| for (const network::mojom::PermissionsPolicyFeature feature : |
| computed_fenced_frame_properties->effective_enabled_permissions()) { |
| if (!IsFencedFrameRequiredPolicyFeatureAllowed(origin, feature)) { |
| const blink::PermissionsPolicyFeatureToNameMap& feature_to_name_map = |
| blink::GetPermissionsPolicyFeatureToNameMap(); |
| const std::string feature_string(feature_to_name_map.at(feature)); |
| frame_tree_node_->current_frame_host()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf( |
| "Refused to frame '%s' as a fenced frame because permissions " |
| "policy '%s' is not allowed for the frame's origin.", |
| origin.Serialize().c_str(), feature_string.c_str())); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| std::unique_ptr<viz::PeakGpuMemoryTracker> |
| NavigationRequest::TakePeakGpuMemoryTracker() { |
| return std::move(loading_mem_tracker_); |
| } |
| |
| std::unique_ptr<NavigationEarlyHintsManager> |
| NavigationRequest::TakeEarlyHintsManager() { |
| return std::move(early_hints_manager_); |
| } |
| |
| network::mojom::ClientSecurityStatePtr |
| NavigationRequest::BuildClientSecurityStateForNavigationFetch() { |
| switch (GetNavigatingFrameType()) { |
| // The client [1] of the navigation fetch request is the navigation |
| // initiator, so use the initiator's policies to set the |
| // `ClientSecurityState`. |
| // |
| // [1] https://fetch.spec.whatwg.org/#concept-request-client |
| // |
| // The `kPrimaryMainFrame` case also covers guest views |
| // (https://crbug.com/1261928) when the MPArch implementation is not being |
| // used. |
| // |
| // TODO(crbug.com/40258826): Determine how to treat guest views. |
| case FrameType::kPrimaryMainFrame: |
| case FrameType::kGuestMainFrame: |
| case FrameType::kSubframe: { |
| if (!policy_container_builder_->InitiatorPolicies()) { |
| return nullptr; |
| } |
| |
| network::mojom::ClientSecurityStatePtr state = DeriveClientSecurityState( |
| *policy_container_builder_->InitiatorPolicies(), |
| PrivateNetworkRequestContext::kNavigation); |
| |
| // Remove the initiator's COEP, it is unused. For iframes, the parent's |
| // COEP should be used: that is checked in `EnforceCOEP()`. The value |
| // in `ClientSecurityState` is used for subresources only, in which case |
| // the network service performs the check on behalf of the client. |
| state->cross_origin_embedder_policy = |
| network::CrossOriginEmbedderPolicy(); |
| |
| return state; |
| } |
| |
| // Fenced frames can only be navigated in two ways: |
| // |
| // 1. By the embedder document, via the fencedframe.config attribute. |
| // The implementation uses the parent policies directly, because |
| // initiator policies are not currently plumbed in this case. This is |
| // correct anyway because the initiator is the parent. |
| // Note: contrary to an iframe, the navigation can never happens at |
| // distance, using e.g. `window.open(url, target)` or `<a target>`. |
| // |
| // 2. By a document in the <fencedframe> frame tree. In this case the |
| // initiator policies are properly plumbed and should be used. |
| // |
| // TODO(crbug.com/40258851): Use the initiator policies. On can use |
| // `is_embedder_initiated_fenced_frame_navigation_` to discriminate (1) from |
| // (2). |
| // |
| // NOTE: For an embedder initiated fenced frame navigation that is subject |
| // to private network access checks: |
| // |
| // 1. The preflight request is sent with an opaque origin: "Origin: null". |
| // See: `FencedFrame::Navigate()`. |
| // 2. The credentials mode of the preflight request is "include". This |
| // prevents response header `Access-Control-Allow-Origin: '*'` from |
| // working. The response header must explicitly specify the origin. |
| // 3. However, we cannot know the origin because of (1). |
| // 4. It is also unsafe to respond to the preflight with response header |
| // `Access-Control-Allow-Origin: 'null'`. See: |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin |
| // |
| // This implies there is a limitation for fenced frames that send a |
| // preflight request because of private network access. Fenced frame |
| // embedder initiated private network accesses always fail. |
| // |
| // NOTE: Fenced frames always have an outer document, |
| // `GetParentFrameOrOuterDocument()` is never nullptr. |
| case FrameType::kFencedFrameRoot: { |
| auto client_security_state = |
| GetParentFrameOrOuterDocument()->BuildClientSecurityState(); |
| |
| // TODO(crbug.com/40258851): Remove COEP from |
| // `client_security_state`, see the reasoning for subframes above. |
| |
| // TODO(crbug.com/40258851): Consider enabling PNA for fenced |
| // frames independently of PNA for iframes. |
| client_security_state->private_network_request_policy = |
| DerivePrivateNetworkRequestPolicy( |
| client_security_state->ip_address_space, |
| client_security_state->is_web_secure_context, |
| PrivateNetworkRequestContext::kNavigation); |
| |
| return client_security_state; |
| } |
| |
| // TODO(crbug.com/40258824): Determine how to treat prerendered |
| // main frames. |
| case FrameType::kPrerenderMainFrame: |
| return nullptr; |
| } |
| } |
| |
| network::mojom::ClientSecurityStatePtr |
| NavigationRequest::BuildClientSecurityStateForCommittedDocument() { |
| const PolicyContainerPolicies& policies = |
| policy_container_builder_->FinalPolicies(); |
| |
| return network::mojom::ClientSecurityState::New( |
| policies.cross_origin_embedder_policy, policies.is_web_secure_context, |
| policies.ip_address_space, private_network_request_policy_, |
| policies.document_isolation_policy); |
| } |
| |
| std::string NavigationRequest::GetUserAgentOverride() { |
| return is_overriding_user_agent() |
| ? frame_tree_node_->navigator() |
| .GetDelegate() |
| ->GetUserAgentOverride(frame_tree_node_->frame_tree()) |
| .ua_string_override |
| : std::string(); |
| } |
| |
| NavigationControllerImpl* NavigationRequest::GetNavigationController() const { |
| return &frame_tree_node_->navigator().controller(); |
| } |
| |
| PrerenderHostRegistry& NavigationRequest::GetPrerenderHostRegistry() { |
| PrerenderHostRegistry* registry = frame_tree_node_->current_frame_host() |
| ->delegate() |
| ->GetPrerenderHostRegistry(); |
| CHECK(registry); |
| return *registry; |
| } |
| |
| mojo::PendingRemote<network::mojom::CookieAccessObserver> |
| NavigationRequest::CreateCookieAccessObserver() { |
| mojo::PendingRemote<network::mojom::CookieAccessObserver> remote; |
| cookie_observers_->Add(remote.InitWithNewPipeAndPassReceiver(), |
| CookieAccessDetails::Source::kNavigation); |
| return remote; |
| } |
| |
| mojo::PendingRemote<network::mojom::TrustTokenAccessObserver> |
| NavigationRequest::CreateTrustTokenAccessObserver() { |
| mojo::PendingRemote<network::mojom::TrustTokenAccessObserver> remote; |
| trust_token_observers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver> |
| NavigationRequest::CreateSharedDictionaryAccessObserver() { |
| mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver> remote; |
| shared_dictionary_observers_.Add(this, |
| remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver> |
| NavigationRequest::CreateDeviceBoundSessionObserver() { |
| mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver> remote; |
| device_bound_session_observers_.Add(this, |
| remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| void NavigationRequest::NotifyCookiesAccessed( |
| std::vector<network::mojom::CookieAccessDetailsPtr> details_vector, |
| CookieAccessDetails::Source source) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationRequest::NotifyCookiesAccessed", |
| TRACE_ID_WITH_SCOPE(kNavigationRequestScope, |
| TRACE_ID_LOCAL(navigation_id_)), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| std::optional<base::ElapsedTimer> timer; |
| if (base::ShouldRecordSubsampledMetric(0.01)) { |
| timer.emplace(); |
| } |
| for (auto& details : details_vector) { |
| // TODO(crbug.com/40520047): We should not send information to the current |
| // frame about (potentially unrelated) ongoing navigation, but at the moment |
| // we don't have another way to add messages to DevTools console. |
| EmitCookieWarningsAndMetrics(frame_tree_node()->current_frame_host(), |
| details); |
| |
| CookieAccessDetails allowed; |
| CookieAccessDetails blocked; |
| SplitCookiesIntoAllowedAndBlocked(details, source, &allowed, &blocked); |
| if (!allowed.cookie_access_result_list.empty()) { |
| GetDelegate()->OnCookiesAccessed(this, allowed); |
| } |
| if (!blocked.cookie_access_result_list.empty()) { |
| GetDelegate()->OnCookiesAccessed(this, blocked); |
| } |
| |
| // When determining the BFCache eligibility, we explicitly ignore the cookie |
| // changes from the navigation itself because we want the |
| // `CookieChangeListener` to only track the cookie changes that potentially |
| // make the document initially rendered by the navigation request outdated. |
| if (allowed.type == CookieAccessDetails::Type::kChange) { |
| uint64_t cookie_modification_count = |
| allowed.cookie_access_result_list.size(); |
| uint64_t http_only_cookie_modification_count = 0u; |
| for (const net::CookieWithAccessResult& cookie_with_access_result : |
| allowed.cookie_access_result_list) { |
| if (cookie_with_access_result.cookie.IsHttpOnly()) { |
| http_only_cookie_modification_count++; |
| } |
| } |
| if (cookie_change_listener_) { |
| cookie_change_listener_->RemoveNavigationCookieModificationCount( |
| base::PassKey<NavigationRequest>(), cookie_modification_count, |
| http_only_cookie_modification_count); |
| } |
| } |
| } |
| if (timer) { |
| base::UmaHistogramCustomMicrosecondsTimes( |
| "Browser.CookieAccessObserver.NavigationRequest.Duration.Subsampled", |
| timer->Elapsed(), base::Microseconds(1), base::Seconds(1), 100); |
| } |
| } |
| |
| std::vector< |
| std::pair<mojo::PendingReceiver<network::mojom::CookieAccessObserver>, |
| CookieAccessDetails::Source>> |
| NavigationRequest::TakeCookieObservers() { |
| return cookie_observers_->TakeReceiversWithContext(); |
| } |
| |
| void NavigationRequest::OnTrustTokensAccessed( |
| network::mojom::TrustTokenAccessDetailsPtr details) { |
| GetDelegate()->OnTrustTokensAccessed(this, TrustTokenAccessDetails(details)); |
| } |
| |
| void NavigationRequest::Clone( |
| mojo::PendingReceiver<network::mojom::TrustTokenAccessObserver> observer) { |
| trust_token_observers_.Add(this, std::move(observer)); |
| } |
| |
| std::vector<mojo::PendingReceiver<network::mojom::TrustTokenAccessObserver>> |
| NavigationRequest::TakeTrustTokenObservers() { |
| return trust_token_observers_.TakeReceivers(); |
| } |
| |
| void NavigationRequest::OnSharedDictionaryAccessed( |
| network::mojom::SharedDictionaryAccessDetailsPtr details) { |
| GetDelegate()->OnSharedDictionaryAccessed(this, *details); |
| } |
| |
| void NavigationRequest::Clone( |
| mojo::PendingReceiver<network::mojom::SharedDictionaryAccessObserver> |
| observer) { |
| shared_dictionary_observers_.Add(this, std::move(observer)); |
| } |
| |
| void NavigationRequest::OnDeviceBoundSessionAccessed( |
| const net::device_bound_sessions::SessionAccess& access) { |
| GetDelegate()->OnDeviceBoundSessionAccessed(this, access); |
| } |
| void NavigationRequest::Clone( |
| mojo::PendingReceiver<network::mojom::DeviceBoundSessionAccessObserver> |
| observer) { |
| device_bound_session_observers_.Add(this, std::move(observer)); |
| } |
| |
| std::vector< |
| mojo::PendingReceiver<network::mojom::SharedDictionaryAccessObserver>> |
| NavigationRequest::TakeSharedDictionaryAccessObservers() { |
| return shared_dictionary_observers_.TakeReceivers(); |
| } |
| |
| std::vector< |
| mojo::PendingReceiver<network::mojom::DeviceBoundSessionAccessObserver>> |
| NavigationRequest::TakeDeviceBoundSessionAccessObservers() { |
| return device_bound_session_observers_.TakeReceivers(); |
| } |
| |
| RenderFrameHostImpl* NavigationRequest::GetInitiatorDocumentRenderFrameHost() { |
| return initiator_document_token_ |
| ? RenderFrameHostImpl::FromDocumentToken( |
| initiator_process_id_, *initiator_document_token_) |
| : nullptr; |
| } |
| |
| void NavigationRequest::RecordAddressSpaceFeature() { |
| DCHECK(response_head_); |
| DCHECK(policy_container_builder_); |
| |
| RenderFrameHostImpl* initiator_render_frame_host = |
| GetInitiatorDocumentRenderFrameHost(); |
| if (!initiator_render_frame_host) { |
| // The initiator document is no longer available, so we cannot log a feature |
| // use against it. This case may result in a slight undercounting, but is |
| // expected to be rare enough that it should not matter for compat risk |
| // evaluation. |
| return; |
| } |
| |
| // If there is an initiator document, then `initiator_frame_token_` should |
| // have a value, and thus there should be initiator policies. |
| const PolicyContainerPolicies* initiator_policies = |
| policy_container_builder_->InitiatorPolicies(); |
| DCHECK(initiator_policies); |
| if (!initiator_policies) { |
| base::debug::DumpWithoutCrashing(); // Just in case. |
| return; |
| } |
| |
| std::optional<blink::mojom::WebFeature> optional_feature = |
| blink::AddressSpaceFeature(blink::FetchType::kNavigation, |
| initiator_policies->ip_address_space, |
| initiator_policies->is_web_secure_context, |
| response_head_->response_address_space); |
| if (!optional_feature.has_value()) { |
| return; |
| } |
| |
| ContentBrowserClient* client = GetContentClient()->browser(); |
| client->LogWebFeatureForCurrentPage(initiator_render_frame_host, |
| *optional_feature); |
| client->LogWebFeatureForCurrentPage( |
| initiator_render_frame_host, |
| IsInOutermostMainFrame() |
| ? blink::mojom::WebFeature::kPrivateNetworkAccessFetchedTopFrame |
| : blink::mojom::WebFeature::kPrivateNetworkAccessFetchedSubFrame); |
| } |
| |
| void NavigationRequest::ComputePoliciesToCommit() { |
| const auto& url = common_params_->url; |
| |
| network::mojom::IPAddressSpace response_address_space = |
| CalculateIPAddressSpace(url, response_head_.get(), |
| GetContentClient()->browser()); |
| policy_container_builder_->SetIPAddressSpace(response_address_space); |
| |
| if (!devtools_instrumentation::ShouldBypassCSP(*this)) { |
| if (response_head_) { |
| policy_container_builder_->AddContentSecurityPolicies( |
| mojo::Clone(response_head_->parsed_headers->content_security_policy)); |
| } |
| } |
| |
| // Use the unchecked / non-sandboxed origin to calculate potential |
| // trustworthiness. Indeed, the potential trustworthiness check should apply |
| // to the origin of the creation URL, prior to opaquification. |
| policy_container_builder_->SetIsOriginPotentiallyTrustworthy( |
| network::IsOriginPotentiallyTrustworthy( |
| GetOriginForURLLoaderFactoryUnchecked())); |
| |
| policy_container_builder_->SetCrossOriginEmbedderPolicy( |
| ComputeCrossOriginEmbedderPolicy()); |
| |
| IntegrityPolicies integrity_policies = ComputeIntegrityPolicies(); |
| policy_container_builder_->SetIntegrityPolicy( |
| std::move(integrity_policies.enforced)); |
| policy_container_builder_->SetIntegrityPolicyReportOnly( |
| std::move(integrity_policies.report_only)); |
| |
| // If the navigation is the result of a network response, set DIP to the |
| // one in the network response. Otherwise, DIP should follow normal rules of |
| // policy inheritance, which should be handled by the policy container. |
| if (response_head_) { |
| SanitizeDocumentIsolationPolicyHeader(); |
| policy_container_builder_->SetDocumentIsolationPolicy( |
| response_head_->parsed_headers->document_isolation_policy); |
| } |
| |
| DCHECK(commit_params_); |
| DCHECK(!HasCommitted()); |
| DCHECK(!IsErrorPage()); |
| |
| policy_container_builder_->ComputePolicies( |
| this, IsMhtmlOrSubframe(), commit_params_->frame_policy.sandbox_flags, |
| is_credentialless()); |
| } |
| |
| void NavigationRequest::ComputePoliciesToCommitForError() { |
| CHECK(!IsMhtmlOrSubframe()); |
| policy_container_builder_->ComputePoliciesForError(); |
| } |
| |
| void NavigationRequest::CheckStateTransition(NavigationState state) const { |
| #if DCHECK_IS_ON() |
| // See |
| // https://chromium.googlesource.com/chromium/src/+/HEAD/docs/navigation-request-navigation-state.png |
| // clang-format off |
| static const base::NoDestructor<base::StateTransitions<NavigationState>> |
| transitions(base::StateTransitions<NavigationState>({ |
| {NOT_STARTED, { |
| WAITING_FOR_RENDERER_RESPONSE, |
| WILL_START_NAVIGATION, |
| WILL_START_REQUEST, |
| }}, |
| {WAITING_FOR_RENDERER_RESPONSE, { |
| WILL_START_NAVIGATION, |
| WILL_START_REQUEST, |
| }}, |
| {WILL_START_NAVIGATION, { |
| WILL_START_REQUEST, |
| WILL_FAIL_REQUEST, |
| }}, |
| {WILL_START_REQUEST, { |
| WILL_REDIRECT_REQUEST, |
| WILL_PROCESS_RESPONSE, |
| WILL_COMMIT_WITHOUT_URL_LOADER, |
| READY_TO_COMMIT, |
| DID_COMMIT, |
| CANCELING, |
| WILL_FAIL_REQUEST, |
| DID_COMMIT_ERROR_PAGE, |
| }}, |
| {WILL_REDIRECT_REQUEST, { |
| WILL_REDIRECT_REQUEST, |
| WILL_PROCESS_RESPONSE, |
| CANCELING, |
| WILL_FAIL_REQUEST, |
| }}, |
| {WILL_PROCESS_RESPONSE, { |
| READY_TO_COMMIT, |
| CANCELING, |
| WILL_FAIL_REQUEST, |
| }}, |
| {WILL_COMMIT_WITHOUT_URL_LOADER, { |
| READY_TO_COMMIT, |
| CANCELING, |
| WILL_FAIL_REQUEST, |
| }}, |
| {READY_TO_COMMIT, { |
| NOT_STARTED, |
| DID_COMMIT, |
| DID_COMMIT_ERROR_PAGE, |
| }}, |
| {CANCELING, { |
| READY_TO_COMMIT, |
| WILL_FAIL_REQUEST, |
| }}, |
| {WILL_FAIL_REQUEST, { |
| READY_TO_COMMIT, |
| CANCELING, |
| WILL_FAIL_REQUEST, |
| }}, |
| {DID_COMMIT, {}}, |
| {DID_COMMIT_ERROR_PAGE, {}}, |
| })); |
| // clang-format on |
| DCHECK_STATE_TRANSITION(transitions, state_, state); |
| #endif // DCHECK_IS_ON() |
| } |
| |
| void NavigationRequest::SetState(NavigationState state) { |
| CheckStateTransition(state); |
| state_ = state; |
| } |
| |
| bool NavigationRequest::MaybeCancelFailedNavigation() { |
| // TODO(crbug.com/41349746): Maybe take `ThrottleCheckResult::action()` into |
| // account as well. |
| // If the request was canceled by the user, do not show an error page. |
| if (net::ERR_ABORTED == net_error_ || |
| // Some embedders suppress error pages to allow custom error handling. |
| silently_ignore_errors_ || |
| // <webview> guests suppress net::ERR_BLOCKED_BY_CLIENT. |
| (net::ERR_BLOCKED_BY_CLIENT == net_error_ && |
| silently_ignore_blocked_by_client_)) { |
| frame_tree_node_->ResetNavigationRequest( |
| NavigationDiscardReason::kInternalCancellation); |
| return true; |
| } |
| |
| // Per https://whatwg.org/C/iframe-embed-object.html#the-object-element, |
| // this implements step 4.7 from "determine what the object element |
| // represents": "If the load failed (e.g. there was an HTTP 404 error, there |
| // was a DNS error), fire an event named error at the element, then jump to |
| // the step below labeled fallback." |
| // |
| // This case handles navigation failure, e.g. due to the navigation being |
| // blocked by WebRequest, DNS errors, et cetera. |
| if (frame_tree_node()->frame_owner_element_type() == |
| blink::FrameOwnerElementType::kObject) { |
| RenderFallbackContentForObjectTag(); |
| frame_tree_node_->ResetNavigationRequest( |
| NavigationDiscardReason::kInternalCancellation); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool NavigationRequest::ShouldRenderFallbackContentForResponse( |
| const net::HttpResponseHeaders& http_headers) const { |
| return frame_tree_node()->frame_owner_element_type() == |
| blink::FrameOwnerElementType::kObject && |
| !network::IsSuccessfulStatus(http_headers.response_code()); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigating-across-documents:hh-replace |
| bool NavigationRequest::ShouldReplaceCurrentEntryForSameUrlNavigation() const { |
| DCHECK_LE(state_, WILL_START_NAVIGATION); |
| |
| // Not a same-url navigation. Note that this is comparing against the "last |
| // loading URL" since this is what was used in the renderer check that was |
| // moved here. This means for error pages we should compare against the URL |
| // that failed to load (the last committed URL), while for other navigations |
| // we should compare against the last document URL, which might be different |
| // from the last committed URL due to document.open() changing the URL. |
| if (common_params_->url != |
| GetLastLoadingURLInRendererForNavigationReplacement( |
| frame_tree_node_->current_frame_host())) { |
| return false; |
| } |
| |
| if (IsLoadDataWithBaseURL()) { |
| // Preserve old behavior of loadDataWithBaseURL() navigations, which almost |
| // never does same-URL replacement when the (data) URL is the same, since it |
| // used to compare the data URL against the "history URL", which in almost |
| // all cases wouldn't match the data: URL (in most cases it's either the |
| // same as the base/document URL, or about:blank [the default value], see |
| // https://crbug.com/1244746#c1 for more details). |
| return false; |
| } |
| |
| // Never replace if there is no NavigationEntry to replace. |
| if (!frame_tree_node_->navigator().controller().GetEntryCount()) |
| return false; |
| |
| // The NavigationAPI allows a page to request a navigation that pushes even in |
| // situations where the browser would implicitly convert the navigation to |
| // a replace. |
| if (begin_params_->force_history_push == |
| blink::mojom::ForceHistoryPush::kYes) { |
| return false; |
| } |
| |
| // Only (1) cross-document navigations and (2) same-document navigations from |
| // browser UI (e.g., address bar or bookmark) need to consider replacing the |
| // entry for same URL cases. Reloads and history navigations have special |
| // handling and don't need to. Note that same-document navigations with |
| // fragments from browser UI are not treated as reloads. |
| // Note that for same document navigation, even though the navigation request |
| // starts with should_replace_current_entry, no new history entry is created. |
| // With the logic in RenderFrameImpl::MakeDidCommitProvisionalLoadParams, we |
| // scroll to the fragment without change to navigation history. |
| // LocalFrame::ShouldReplaceForSameUrlNavigation also returns true for |
| // renderer initiated same url navigation within the same document. |
| const ui::PageTransition transition = |
| ui::PageTransitionFromInt(common_params_->transition); |
| const bool is_same_document_navigation_from_browser_ui = |
| (common_params_->navigation_type == |
| blink::mojom::NavigationType::SAME_DOCUMENT) && |
| ((transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) || |
| PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_AUTO_BOOKMARK)); |
| if (!is_same_document_navigation_from_browser_ui && |
| (common_params_->navigation_type != |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT)) { |
| return false; |
| } |
| // Form submissions to the same url should not replace. |
| if (begin_params_->is_form_submission) |
| return false; |
| |
| // If the initiating frame is cross-origin to the target frame, do not |
| // replace. Replacing in this case can be used to guess the exact current url |
| // of a cross-origin frame, see https://crbug.com/1208614. Exempt error pages |
| // from this rule so that we don't leave an error page in the back/forward |
| // list if a cross-origin iframe happens to successfully re-naivgate a frame |
| // that had previously failed. |
| if (!frame_tree_node_->current_frame_host()->IsErrorDocument() && |
| common_params_->initiator_origin && |
| !common_params_->initiator_origin->IsSameOriginWith( |
| frame_tree_node_->current_origin())) { |
| return false; |
| } |
| |
| // Otherwise, replace current entry. |
| return true; |
| } |
| |
| bool NavigationRequest:: |
| ShouldReplaceCurrentEntryForNavigationFromInitialEmptyDocumentOrEntry() |
| const { |
| DCHECK_LE(state_, WILL_START_NAVIGATION); |
| // Never replace if there is no NavigationEntry to replace. |
| if (!frame_tree_node_->navigator().controller().GetEntryCount()) |
| return false; |
| |
| if (common_params_->navigation_type != |
| blink::mojom::NavigationType::SAME_DOCUMENT && |
| common_params_->navigation_type != |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT) { |
| // History navigations, session restore, and reloads shouldn't do |
| // replacement. |
| return false; |
| } |
| |
| // Check the "initial NavigationEntry" status and the "initial empty document" |
| // status. |
| |
| if (frame_tree_node_->navigator() |
| .controller() |
| .GetLastCommittedEntry() |
| ->IsInitialEntry()) { |
| // Initial NavigationEntry must always be replaced, to ensure that as long |
| // as the NavigationEntry exists, it will be the only NavigationEntry in |
| // the session history list, making history navigations to initial |
| // NavigationEntry possible. See the comment for `is_initial_entry_` in |
| // NavigationEntryImpl for more details. |
| return true; |
| } |
| |
| // For non-initial NavigationEntries, the initial empty document should also |
| // be replaced in subframes and non-outermost main frames (fenced frames). |
| // For outermost main frames, the initial empty document will usually only |
| // exist when on the initial NavigationEntry, but it can also exist in a |
| // restored or cloned NavigationController before the first commit. It is |
| // important not to replace one of the restored entries in that case. See |
| // https://crbug.com/1284566 and https://crbug.com/1295723. |
| return frame_tree_node_->is_on_initial_empty_document() && |
| frame_tree_node_->GetParentOrOuterDocument(); |
| } |
| |
| bool NavigationRequest::ShouldReplaceCurrentEntryForFailedNavigation() const { |
| DCHECK(state_ == CANCELING || state_ == WILL_FAIL_REQUEST); |
| |
| if (common_params_->should_replace_current_entry) |
| return true; |
| |
| // Never replace if there is no NavigationEntry to replace. |
| if (!frame_tree_node_->navigator().controller().GetEntryCount()) |
| return false; |
| |
| auto page_state = |
| blink::PageState::CreateFromEncodedData(commit_params_->page_state); |
| // Failed history navigations with valid PageState should not do replacement. |
| if (page_state.IsValid()) |
| return false; |
| |
| bool is_reload_or_history = |
| NavigationTypeUtils::IsReload(common_params_->navigation_type) || |
| NavigationTypeUtils::IsHistory(common_params_->navigation_type); |
| // Otherwise, these navigations should do replacement: |
| // - Failed history/reloads with invalid PageState (e.g. same-URL navigations |
| // that got converted to a reload). |
| // - Same-URL navigations. Note that even though we had a same-URL check |
| // earlier in the navigation's lifetime (which would convert same-URL |
| // navigations to reload or replacement), those compare against the initial |
| // URL instead of the final URL, which is what we're using here. Also, this |
| // is using the "loading URL", since that is the URL that was used in the |
| // renderer before we moved the replacement conversion here. |
| // TODO(crbug.com/40755155): Reconsider whether these two cases should |
| // do replacement or not, since we're just preserving old behavior here. |
| return is_reload_or_history || |
| (common_params_->url == |
| GetLastLoadingURLInRendererForNavigationReplacement( |
| frame_tree_node_->current_frame_host())); |
| } |
| |
| const std::optional<FencedFrameProperties>& |
| NavigationRequest::ComputeFencedFrameProperties( |
| FencedFramePropertiesNodeSource node_source) const { |
| if (node_source == FencedFramePropertiesNodeSource::kFrameTreeRoot && |
| !frame_tree_node_->IsFencedFrameRoot()) { |
| // Sometimes nodes other than the frame tree root (urn iframes) have |
| // FencedFrameProperties in their navigation requests. When the node source |
| // is kFrameTreeRoot and this navigation request is not for the frame tree |
| // root, get the properties from the frame tree root. |
| return frame_tree_node_->GetFencedFrameProperties(node_source); |
| } |
| |
| if (fenced_frame_properties_) { |
| return fenced_frame_properties_; |
| } |
| |
| return frame_tree_node_->GetFencedFrameProperties(node_source); |
| } |
| |
| const std::optional<base::UnguessableToken> |
| NavigationRequest::ComputeFencedFrameNonce() const { |
| // For partition nonce, all nested frame inside a fenced frame tree should |
| // operate on the partition nonce of the frame tree root. |
| const std::optional<FencedFrameProperties>& computed_fenced_frame_properties = |
| ComputeFencedFrameProperties( |
| /*node_source=*/FencedFramePropertiesNodeSource::kFrameTreeRoot); |
| if (!computed_fenced_frame_properties.has_value()) { |
| return std::nullopt; |
| } |
| if (!computed_fenced_frame_properties->partition_nonce().has_value()) { |
| // It is only possible for there to be `FencedFrameProperties` but no |
| // partition nonce in urn iframes (which could indeed be nested inside a |
| // fenced frame). |
| CHECK(blink::features::IsAllowURNsInIframeEnabled()); |
| return std::nullopt; |
| } |
| return computed_fenced_frame_properties->partition_nonce() |
| ->GetValueIgnoringVisibility(); |
| } |
| |
| void NavigationRequest::RenderFallbackContentForObjectTag() { |
| // https://whatwg.org/C/iframe-embed-object.html#the-object-element:fallback-content-5: |
| // Fallback content is represented by the children of the <object> tag, so it |
| // will be rendered in the process of the parent's document. |
| DCHECK_EQ(blink::FrameOwnerElementType::kObject, |
| frame_tree_node_->frame_owner_element_type()); |
| if (RenderFrameProxyHost* proxy = |
| frame_tree_node_->render_manager()->GetProxyToParent()) { |
| if (proxy->is_render_frame_proxy_live()) { |
| proxy->GetAssociatedRemoteFrame()->RenderFallbackContent(); |
| } |
| } else { |
| frame_tree_node_->current_frame_host() |
| ->GetAssociatedLocalFrame() |
| ->RenderFallbackContent(); |
| } |
| } |
| |
| std::optional<base::UnguessableToken> |
| NavigationRequest::GetNavigationTokenForDeferringSubframes() { |
| DCHECK(IsInMainFrame()); |
| if (!IsSameDocument() || |
| !NavigationTypeUtils::IsHistory(common_params_->navigation_type)) { |
| return std::nullopt; |
| } |
| RenderFrameHostImpl* current_frame_host = |
| frame_tree_node_->current_frame_host(); |
| if (!current_frame_host->has_navigate_event_handler()) { |
| return std::nullopt; |
| } |
| if (commit_params_->is_browser_initiated && |
| !current_frame_host->IsHistoryUserActivationActive()) { |
| return std::nullopt; |
| } |
| return commit_params_->navigation_token; |
| } |
| |
| void NavigationRequest::AddDeferredSubframeNavigationThrottle( |
| base::WeakPtr<SubframeHistoryNavigationThrottle> throttle) { |
| DCHECK(IsInMainFrame()); |
| subframe_history_navigation_throttles_.push_back(throttle); |
| } |
| |
| void NavigationRequest::UnblockPendingSubframeNavigationRequestsIfNeeded() { |
| // After a main frame same-document history navigation completes successfully, |
| // we can resume any corresponding subframe history navigations that were |
| // blocked on it. |
| base::WeakPtr<NavigationRequest> self = GetWeakPtr(); |
| for (auto& throttle : subframe_history_navigation_throttles_) { |
| if (throttle) { |
| throttle->Resume(); |
| if (!self) { |
| return; |
| } |
| } |
| } |
| subframe_history_navigation_throttles_.clear(); |
| } |
| |
| void NavigationRequest::MaybeDispatchNavigateEventForCrossDocumentTraversal() { |
| // If this is a cross-document history navigation, notify the renderer to |
| // fire the navigate event now that we know which frames are navigating and |
| // whether the navigation is same-origin. Note that while the navigate event |
| // can normally intercept or cancel a navigation, it has neither of those |
| // powers for a cross-document history navigation, and therefore can be |
| // dispatched without waiting for a result. The worst it can do is detach the |
| // frame asynchronously, which javascript could do at any time anyway. |
| if (common_params_->navigation_type != |
| blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT) { |
| return; |
| } |
| // Only fire the navigate event if the destination is same-origin. Because |
| // this check is performed at navigation start time, `destination_origin` is |
| // based on the pre-redirect URL, which is consistent with the renderer |
| // process logic for firing the navigate event for non-history navigations. |
| url::Origin destination_origin = url::Origin::Resolve( |
| common_params_->url, |
| common_params_->initiator_origin.value_or(url::Origin())); |
| if (!frame_tree_node_->current_origin().IsSameOriginWith( |
| destination_origin)) { |
| return; |
| } |
| frame_tree_node_->current_frame_host() |
| ->GetAssociatedLocalFrame() |
| ->DispatchNavigateEventForCrossDocumentTraversal( |
| common_params_->url, commit_params_->page_state, |
| commit_params_->is_browser_initiated); |
| } |
| |
| bool NavigationRequest::IsServedFromBackForwardCache() const { |
| return is_back_forward_cache_restore_; |
| } |
| |
| bool NavigationRequest::IsPageActivation() const { |
| return const_cast<NavigationRequest*>(this)->IsPrerenderedPageActivation() || |
| IsServedFromBackForwardCache(); |
| } |
| |
| std::unique_ptr<NavigationEntryImpl> |
| NavigationRequest::TakePrerenderNavigationEntry() { |
| DCHECK(IsPrerenderedPageActivation()); |
| return std::move(prerender_navigation_state_->prerender_navigation_entry); |
| } |
| |
| bool NavigationRequest::IsWaitingForBeforeUnload() { |
| return state_ < WILL_START_NAVIGATION; |
| } |
| |
| void NavigationRequest::AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel level, |
| std::string message) { |
| DCHECK_LE(state_, READY_TO_COMMIT); |
| console_messages_.push_back(ConsoleMessage{level, std::move(message)}); |
| } |
| |
| void NavigationRequest::SendDeferredConsoleMessages() { |
| for (auto& message : console_messages_) { |
| // TODO(crbug.com/40520047): We should have a way of sending console |
| // messaged to devtools without going through the renderer. |
| GetRenderFrameHost()->AddMessageToConsole(message.level, |
| std::move(message.message)); |
| } |
| console_messages_.clear(); |
| } |
| |
| std::optional<AgentClusterKey::CrossOriginIsolationKey> |
| NavigationRequest::ComputeCrossOriginIsolationKey() { |
| // If the final security policies have not been computed yet, return an empty |
| // CrossOriginIsolationKey. This is because we cannot compute the proper |
| // CrossOriginIsolationKey for the navigation yet. |
| // TODO(crbug.com/343914483): When navigating between same-origin documents, |
| // consider passing the CrossOriginIsolationKey of the current document to |
| // avoid creating spurious speculative RFH when navigating between two |
| // same-origin documents with crossOriginIsolation. |
| if (!policy_container_builder_->HasComputedPolicies()) { |
| return std::nullopt; |
| } |
| |
| // If the navigation does not have a Document-Isolation-Policy, return an |
| // empty CrossOriginIsolationKey. |
| // TODO(crbug.com/342365083): Use a CrossOriginIsolationKey when the |
| // navigation has COOP and COEP, or happens in cross-origin isolated |
| // BrowsingInstance. |
| if (policy_container_builder_->FinalPolicies() |
| .document_isolation_policy.value == |
| network::mojom::DocumentIsolationPolicyValue::kNone) { |
| return std::nullopt; |
| } |
| |
| // The document we're navigating to has a DocumentIsolationPolicy of |
| // "isolate-and-require-corp" or "isolate-and-credentialless". This means that |
| // the document requested crossOriginIsolation, so return a cross-origin |
| // isolation key with the current origin. Its cross-origin isolation mode |
| // depends on the capabilities of the platform. Currently, we only support a |
| // cross-origin isolation mode of kConcrete and platforms with full Site |
| // Isolation. |
| // TODO(crbug.com/342364564): Support platforms that do not |
| // support OOPIF and return an AgentClusterKey with a CrossOriginIsolationKey |
| // that has a kLogical cross-origin isolation mode. |
| CHECK(policy_container_builder_->FinalPolicies() |
| .document_isolation_policy.value == |
| network::mojom::DocumentIsolationPolicyValue:: |
| kIsolateAndRequireCorp || |
| policy_container_builder_->FinalPolicies() |
| .document_isolation_policy.value == |
| network::mojom::DocumentIsolationPolicyValue:: |
| kIsolateAndCredentialless); |
| |
| // If the navigation doesn't have an origin, we cannot create a |
| // CrossOriginIsolationKey for it, since it must be tied to an origin. |
| url::Origin origin; |
| if (state_ < WILL_PROCESS_RESPONSE) { |
| origin = GetTentativeOriginAtRequestTime(); |
| } else { |
| std::optional<url::Origin> origin_to_commit = GetOriginToCommit(); |
| if (!origin_to_commit.has_value()) { |
| return std::nullopt; |
| } |
| origin = origin_to_commit.value(); |
| } |
| |
| // Inform the PolicyContainer that DocumentIsolationPolicy has enabled |
| // crossOriginIsolation for the document. |
| policy_container_builder_->SetCrossOriginIsolationEnabledByDIP(); |
| |
| return AgentClusterKey::CrossOriginIsolationKey( |
| origin, CrossOriginIsolationMode::kConcrete); |
| } |
| |
| std::optional<WebExposedIsolationInfo> |
| NavigationRequest::ComputeWebExposedIsolationInfo() { |
| // If we are in an iframe, we inherit the isolation state of the top level |
| // frame. This can be inferred from the main frame SiteInstance. Note that |
| // Iframes have to pass COEP tests in |OnResponseStarted| before being loaded |
| // and inheriting this cross-origin isolated state. |
| // |
| // Embedded content that cannot always provide a separate process (Fenced |
| // frames) should use the crossOriginIsolated state of their |
| // parent. Therefore we use IsOutermostMainFrame. |
| // |
| // TODO(crbug.com/40180791): This may change as we work out the model for |
| // isolation mechanisms beyond "cross-origin isolation". |
| if (!frame_tree_node_->IsOutermostMainFrame()) { |
| return frame_tree_node_->current_frame_host() |
| ->GetMainFrame() |
| ->GetSiteInstance() |
| ->GetWebExposedIsolationInfo(); |
| } |
| |
| // This accommodates for web tests that use COOP. They expect an about:blank |
| // page to stay in process, and hang otherwise. In general, it is safe to |
| // allow about:blank pages to stay in process, since scriptability is limited |
| // to the BrowsingInstance and all pages with the same web-exposed isolation |
| // level are trusted. |
| if (common_params_->url.IsAboutBlank()) |
| return std::nullopt; |
| |
| // If we haven't yet received a definitive network response, it is too early |
| // to guess the isolation state. |
| if (state_ < WILL_PROCESS_RESPONSE) |
| return std::nullopt; |
| |
| // We consider navigations to be cross-origin isolated if the response |
| // asserts proper COOP and COEP headers. |
| if ((coop_status().current_coop().value != |
| network::mojom::CrossOriginOpenerPolicyValue::kSameOriginPlusCoep)) { |
| return WebExposedIsolationInfo::CreateNonIsolated(); |
| } |
| |
| const GURL& url = common_params().url; |
| |
| // TODO(https://crbug.com/415943168): This should take into account Sandbox |
| // Flags. However, we can only do so when the CrossOriginOpenerPolicyStatus |
| // also take them into account. Because the CrossOriginOpenerPolicyStatus does |
| // not take into account sandbox flags, it does not mandate a BrowsingInstance |
| // switch when navigating between two same-origin pages where one of the pages |
| // has sandox flags that make its origin opaque. So the two pages are going to |
| // commit in the same BrowsingInstance. If we use an opaque origin here, we |
| // would end up with a mismatch between the WebExposedIsolationInfo for the |
| // navigation and that of the BrowsingInstance it is set to commit into. |
| const url::Origin origin = GetOriginForURLLoaderFactoryUnchecked(); |
| |
| return SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel( |
| GetNavigationController()->GetBrowserContext(), url) |
| ? WebExposedIsolationInfo::CreateIsolatedApplication(origin) |
| : WebExposedIsolationInfo::CreateIsolated(origin); |
| } |
| |
| void NavigationRequest::MaybeAssignInvalidPrerenderFrameTreeNodeId() { |
| if (!prerender_frame_tree_node_id_.has_value()) { |
| // This navigation won't activate a prerendered page. Otherwise, |
| // `prerender_frame_tree_node_id_` should have already been set before this |
| // in OnPrerenderingActivationChecksComplete(). |
| prerender_frame_tree_node_id_ = FrameTreeNodeId(); |
| } |
| } |
| |
| void NavigationRequest::RendererCancellationWindowEnded() { |
| // The renderer had indicated that the navigation cancellation window had |
| // ended, so the navigation can resume if it is currently waiting for this |
| // signal. |
| renderer_cancellation_window_ended_ = true; |
| renderer_cancellation_listener_.reset(); |
| if (renderer_cancellation_window_ended_callback_) { |
| std::move(renderer_cancellation_window_ended_callback_).Run(); |
| // DO NOT ADD CODE after this. The callback triggers |
| // RendererCancellationThrottle::NavigationCancellationWindowEnded() and |
| // eventually NavigationThrottle::Resume(), which might have destroyed the |
| // NavigationRequest. |
| } |
| } |
| |
| bool NavigationRequest::ShouldWaitForRendererCancellationWindowToEnd() { |
| return renderer_cancellation_listener_.is_bound() && |
| !renderer_cancellation_window_ended_; |
| } |
| |
| NavigationRequest::ScopedCrashKeys::ScopedCrashKeys( |
| NavigationRequest& navigation_request) |
| : initiator_origin_( |
| GetNavigationRequestInitiatorCrashKey(), |
| base::OptionalToPtr(navigation_request.GetInitiatorOrigin())), |
| url_(GetNavigationRequestUrlCrashKey(), navigation_request.GetURL()), |
| is_same_document_( |
| GetNavigationRequestIsSameDocumentCrashKey(), |
| navigation_request.IsSameDocument() ? "same-doc" : "cross-doc") {} |
| |
| NavigationRequest::ScopedCrashKeys::~ScopedCrashKeys() = default; |
| |
| PreloadingTriggerType NavigationRequest::GetPrerenderTriggerType() { |
| DCHECK(prerender_trigger_type_.has_value()); |
| return prerender_trigger_type_.value(); |
| } |
| |
| std::string NavigationRequest::GetPrerenderEmbedderHistogramSuffix() { |
| return prerender_embedder_histogram_suffix_; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| const base::android::JavaRef<jobject>& |
| NavigationRequest::GetJavaNavigationHandle() { |
| return navigation_handle_proxy_->java_navigation_handle(); |
| } |
| #endif |
| |
| void NavigationRequest::SetViewTransitionState( |
| std::unique_ptr<ScopedViewTransitionResources> resources, |
| blink::ViewTransitionState view_transition_state) { |
| commit_params_->view_transition_state = std::move(view_transition_state); |
| CHECK(resources); |
| view_transition_resources_ = std::move(resources); |
| } |
| |
| void NavigationRequest::ResetViewTransitionState() { |
| if (!commit_params_->view_transition_state) { |
| CHECK(!view_transition_resources_); |
| return; |
| } |
| |
| CHECK(view_transition_resources_); |
| commit_params_->view_transition_state.reset(); |
| view_transition_resources_.reset(); |
| |
| // If we cached a view transition for the old Document and the transition |
| // has been aborted, inform the old Document to discard the pending |
| // ViewTransition. |
| // |
| // Note: If the transition is aborted before the renderer acks the |
| // snapshot IPC, we won't have any resources here. The |
| // ViewTransitionCommitDeferringCondition is responsible for discarding the |
| // pending transition in this case. |
| if (auto* previous_rfh = |
| RenderFrameHostImpl::FromID(GetPreviousRenderFrameHostId())) { |
| previous_rfh->GetAssociatedLocalFrame() |
| ->NotifyViewTransitionAbortedToOldDocument(); |
| } |
| } |
| |
| bool NavigationRequest::IsDisabledEmbedderInitiatedFencedFrameNavigation() { |
| // The untrusted network access check only applies to embedder-initiated |
| // fenced frame root navigations. Note that |
| // `is_embedder_initiated_fenced_frame_navigation_` being true includes fenced |
| // frame and urn iframe embedder initiated navigations, so we need the |
| // additional `IsFencedFrameRoot` check. |
| if (frame_tree_node_->IsFencedFrameRoot() && |
| is_embedder_initiated_fenced_frame_navigation_ && |
| base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesLocalUnpartitionedDataAccess)) { |
| const std::optional<FencedFrameProperties>& |
| embedder_fenced_frame_properties = GetParentFrameOrOuterDocument() |
| ->frame_tree_node() |
| ->GetFencedFrameProperties(); |
| const std::optional<FencedFrameProperties>& target_fenced_frame_properties = |
| frame_tree_node_->GetFencedFrameProperties( |
| FencedFramePropertiesNodeSource::kFrameTreeRoot); |
| |
| if (target_fenced_frame_properties.has_value() && |
| target_fenced_frame_properties |
| ->HasDisabledNetworkForCurrentFrameTree() && |
| embedder_fenced_frame_properties.has_value() && |
| embedder_fenced_frame_properties |
| ->HasDisabledNetworkForCurrentFrameTree()) { |
| // Navigation should be aborted if: |
| // 1. The nested fenced frame has disabled the untrusted network access. |
| // 2. The embedder fenced frame has disabled the untrusted network access |
| // after the navigation starts. |
| // |
| // Note: The navigation is allowed if only embedder fenced frame has |
| // disabled the untrusted network access. This allows the fenced frame |
| // to navigate its nested fenced frame to a nested config while the parent |
| // fenced frame disables its own network. |
| // |
| // After top-level FF disables its network, the nested FF's navigation |
| // may not have committed yet. Top-level FF has no way of knowing when it |
| // is safe to disable network for nested FF (and it would be a privacy |
| // violation for it to know), so nested FF has to disable network for |
| // itself if it wants to get shared storage access. |
| // |
| // For top-level FF, it does not have shared storage access until all |
| // its descendants have also disabled network. |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| blink::RuntimeFeatureStateContext& |
| NavigationRequest::GetMutableRuntimeFeatureStateContext() { |
| // runtime_feature_state_context_ shouldn't be modified after READY_TO_COMMIT |
| // as its state has already been sent to the renderer. |
| DCHECK_LE(state_, NavigationState::READY_TO_COMMIT); |
| return runtime_feature_state_context_; |
| } |
| |
| const blink::RuntimeFeatureStateContext& |
| NavigationRequest::GetRuntimeFeatureStateContext() { |
| return runtime_feature_state_context_; |
| } |
| |
| // The NavigationDownloadPolicy is currently computed by the renderer process. |
| // The problem: not every navigation are initiated from the renderer. Most |
| // fields from the bitfield can be computed from the browser process. This |
| // function is a partial attempt at doing it. |
| void NavigationRequest::ComputeDownloadPolicy() { |
| // [ViewSource] |
| if (GetNavigationEntry() && GetNavigationEntry()->IsViewSourceMode()) { |
| download_policy().SetDisallowed(blink::NavigationDownloadType::kViewSource); |
| } |
| |
| // [Sandbox] |
| if ((commit_params_->frame_policy.sandbox_flags & |
| network::mojom::WebSandboxFlags::kDownloads) == |
| network::mojom::WebSandboxFlags::kDownloads) { |
| download_policy().SetDisallowed(blink::NavigationDownloadType::kSandbox); |
| } |
| |
| // TODO(arthursonzogni): Check if the following fields from the |
| // NavigationDownloadPolicy could be computed here from the browser process |
| // instead: |
| // |
| // [NoGesture] |
| // [OpenerCrossOrigin] |
| // [AdFrameNoGesture] |
| // [AdFrame] |
| // [Interstitial] |
| } |
| |
| bool NavigationRequest::ShouldQueueDueToExistingPendingCommitRFH() const { |
| CHECK_EQ(this, frame_tree_node_->navigation_request()); |
| CHECK(state_ < READY_TO_COMMIT || state_ == WILL_FAIL_REQUEST); |
| |
| if (RenderFrameHostImpl* speculative_rfh = |
| frame_tree_node_->render_manager()->speculative_frame_host()) { |
| // Queue the navigation if there is a pending commit RenderFrameHost. |
| return speculative_rfh->HasPendingCommitForCrossDocumentNavigation(); |
| } |
| return false; |
| } |
| |
| void NavigationRequest::RecordMetricsForBlockedGetFrameHostAttempt( |
| bool commit_attempt) { |
| DCHECK(!pending_commit_metrics_.start_time.is_null()); |
| ++pending_commit_metrics_.blocked_count; |
| if (commit_attempt) { |
| ++pending_commit_metrics_.blocked_commit_count; |
| } |
| } |
| |
| void NavigationRequest::PostResumeCommitTask() { |
| DCHECK(ShouldAvoidRedundantNavigationCancellations()); |
| DCHECK(!ShouldQueueDueToExistingPendingCommitRFH()); |
| // TODO(crbug.com/40186427): Add some metrics for how often: |
| // - this is run |
| // - how long navigations remain queued |
| // - how often it ends up having to simply re-queue itself |
| if (resume_commit_closure_) { |
| // Post a task so that we resume the navigation asynchronously. Note |
| // that we're guaranteed to not have a new RFH get into a pending commit |
| // stage in between the time we post this task and the time we run it. |
| // `this` is the previously-queued NavigationRequest and is still owned by |
| // the `FrameTreeNode`. If a new `NavigationRequest` is created, it will |
| // replace and delete `this` and the resume callback for `this` will be |
| // skipped. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostNonNestableTask( |
| FROM_HERE, std::move(resume_commit_closure_)); |
| } |
| } |
| |
| void NavigationRequest::CheckSoftNavigationHeuristicsInvariants() { |
| if (!commit_params_->soft_navigation_heuristics_task_id) { |
| return; |
| } |
| // TODO(crbug.com/40283341): Checking for a restore here, to |
| // accommodate for restored same document navigations. They are currently |
| // NOT executed as same-document. The task ID is cleared to ensure it never |
| // leak toward a different document. |
| if (IsRestore()) { |
| DCHECK(!IsSameDocument()); |
| commit_params_->soft_navigation_heuristics_task_id = {}; |
| return; |
| } |
| |
| // In NavigationControllerImpl::NavigateToExistingPendingEntry we're verifying |
| // that the task ID is only passed along if the initiator RFH is the same as |
| // the navigated RFH. |
| DCHECK(IsSameDocument()); |
| DCHECK(IsInMainFrame()); |
| DCHECK(!frame_tree_node()->IsFencedFrameRoot()); |
| } |
| |
| StoragePartition* NavigationRequest::GetStoragePartitionWithCurrentSiteInfo() { |
| // `site_info_`'s StoragePartitionConfig should refer to the correct |
| // `StoragePartition` for this navigation. |
| return frame_tree_node_->navigator() |
| .controller() |
| .GetBrowserContext() |
| ->GetStoragePartition(site_info_.storage_partition_config()); |
| } |
| |
| void NavigationRequest::CreateWebUIIfNeeded(RenderFrameHostImpl* frame_host) { |
| TRACE_EVENT1("content", "NavigationRequest::CreateWebUI", "url", GetURL()); |
| |
| WebUI::TypeID new_web_ui_type = |
| WebUIControllerFactoryRegistry::GetInstance()->GetWebUIType( |
| frame_tree_node_->navigator().controller().GetBrowserContext(), |
| GetURL()); |
| if (new_web_ui_type == WebUI::kNoWebUI) { |
| // The navigation doesn't need a WebUI. |
| return; |
| } |
| CHECK(!web_ui_); |
| |
| // We reuse WebUI on navigations with the same WebUI type where we use the |
| // same RFH, so don't create a new one if there is already an existing WebUI |
| // in `frame_host`. However, it is useful to verify that its type hasn't |
| // changed. Site isolation guarantees that RenderFrameHostImpl will be changed |
| // if the WebUI type differs. |
| if (frame_host && frame_host->web_ui()) { |
| CHECK_EQ(new_web_ui_type, frame_host->web_ui_type()); |
| return; |
| } |
| |
| web_ui_ = std::make_unique<WebUIImpl>(this); |
| std::unique_ptr<WebUIController> controller( |
| WebUIControllerFactoryRegistry::GetInstance() |
| ->CreateWebUIControllerForURL(web_ui_.get(), GetURL())); |
| |
| // If we have assigned (zero or more) bindings to the NavigationEntry in |
| // the past, make sure we're not granting it different bindings than it |
| // had before. If so, note it and don't give it any bindings, to avoid a |
| // potential privilege escalation. |
| if (bindings().has_value() && bindings().value() != web_ui_->GetBindings()) { |
| RecordAction(base::UserMetricsAction("ProcessSwapBindingsMismatch_RVHM")); |
| base::WeakPtr<NavigationRequest> self = GetWeakPtr(); |
| // Reset `controller` first before resetting `web_ui_`, since the controller |
| // still has a pointer to `web_ui_`, to avoid referencing to the already |
| // deleted `web_ui_` object from `controller`'s destructor. See also |
| // https://crbug.com/345640549. |
| controller.reset(); |
| web_ui_.reset(); |
| // Resetting the WebUI may indirectly call content's embedders and delete |
| // `this`. There are no known occurrences of it, so we assume this never |
| // happen and crash immediately if it does, because there are no easy ways |
| // to recover. |
| CHECK(self); |
| return; |
| } |
| |
| web_ui_->SetController(std::move(controller)); |
| } |
| |
| bool NavigationRequest::IsDeferred() { |
| return !throttle_registry_->GetDeferringThrottles().empty(); |
| } |
| |
| void NavigationRequest::OnResponseBodyReady(MojoResult) { |
| size_t available_bytes = 0; |
| MojoResult result = response_body().ReadData( |
| MOJO_READ_DATA_FLAG_QUERY, base::span<uint8_t>(), available_bytes); |
| CHECK_EQ(result, MOJO_RESULT_OK); |
| |
| std::string response_body_contents(available_bytes, '\0'); |
| size_t actually_read_bytes = 0; |
| result = response_body().ReadData( |
| MOJO_READ_DATA_FLAG_PEEK, |
| base::as_writable_byte_span(response_body_contents), actually_read_bytes); |
| switch (result) { |
| case MOJO_RESULT_OK: |
| // The watcher is reset before calling the callback since the callback may |
| // resume the throttle. If the watcher is still active, resumption via |
| // callback results in running the OnceCallback twice. |
| response_body_watcher_.reset(); |
| response_body_contents.resize(actually_read_bytes); |
| std::move(response_body_callback_).Run(std::move(response_body_contents)); |
| break; |
| case MOJO_RESULT_SHOULD_WAIT: |
| response_body_watcher_->ArmOrNotify(); |
| break; |
| default: |
| // The watcher is reset before calling the callback since the callback may |
| // resume the throttle. If the watcher is still active, resumption via |
| // callback results in running the OnceCallback twice. |
| response_body_watcher_.reset(); |
| // The client throttle may be waiting for the response body before |
| // resuming navigation, so call the callback with an empty response body |
| // to unblock the throttle. |
| std::move(response_body_callback_).Run(std::string()); |
| break; |
| } |
| } |
| |
| void NavigationRequest::RecordEarlyRenderFrameHostSwapMetrics() { |
| base::UmaHistogramEnumeration("Navigation.EarlyRenderFrameHostSwapType", |
| early_render_frame_host_swap_type_); |
| if (early_render_frame_host_swap_type_ != |
| NavigationRequest::EarlyRenderFrameHostSwapType::kNone) { |
| base::UmaHistogramBoolean( |
| "Navigation.EarlyRenderFrameHostSwap.HasCommitted", HasCommitted()); |
| base::UmaHistogramBoolean( |
| "Navigation.EarlyRenderFrameHostSwap.IsInOutermostMainFrame", |
| IsInOutermostMainFrame()); |
| } |
| } |
| |
| url::Origin NavigationRequest::GetOriginForURLLoaderFactoryUnchecked() { |
| if (DidEncounterError()) { |
| // Error pages commit in an opaque origin in the renderer process. If this |
| // NavigationRequest resulted in committing an error page, return an |
| // opaque origin that has precursor information consistent with the URL |
| // being requested. Note: this is intentionally done first; cases like |
| // errors in srcdoc frames need not inherit the parent's origin for errors. |
| return url::Origin::Create(common_params().url).DeriveNewOpaqueOrigin(); |
| } |
| |
| // Check if this is loadDataWithBaseUrl (which needs special treatment). |
| if (IsLoadDataWithBaseURL()) { |
| // A (potentially attacker-controlled) renderer process should not be able |
| // to use loadDataWithBaseUrl code path to initiate fetches on behalf of a |
| // victim origin (fetches controlled by attacker-provided |
| // |common_params.url| data: URL in a victim's origin from the |
| // attacker-provided |common_params.base_url_for_data_url|). Browser |
| // process should verify that |common_params.base_url_for_data_url| is empty |
| // for all renderer-initiated navigations (e.g. see |
| // VerifyBeginNavigationCommonParams), but as a defense-in-depth this is |
| // also asserted below. |
| // History navigations are exempt from this rule because, although they can |
| // be renderer-initaited via the js history API, the renderer does not |
| // choose the url being navigated to. A renderer-initiated history |
| // navigation may therefore navigate back to a previous browser-initiated |
| // loadDataWithBaseUrl. |
| CHECK(browser_initiated() || |
| NavigationTypeUtils::IsHistory(common_params().navigation_type)); |
| |
| // loadDataWithBaseUrl submits a data: |common_params.url| (which has a |
| // opaque origin), but commits that URL as if it came from |
| // |common_params.base_url_for_data_url|. See also |
| // https://crbug.com/976253. |
| return url::Origin::Create(common_params().base_url_for_data_url); |
| } |
| |
| // Use the cached tentative data origin to commit in the data: URL case. When |
| // there are multiple data: URLs in the same SiteInstanceGroup, we can rely on |
| // the nonce from the origin and that of the site URL to match, which will let |
| // us uniquely identify the correct data: SiteInstance. |
| if (common_params().url.SchemeIs(url::kDataScheme) && |
| tentative_data_origin_to_commit_.has_value()) { |
| return tentative_data_origin_to_commit_.value(); |
| } |
| |
| // Srcdoc subframes need to inherit their origin from their parent frame. |
| if (GetURL().IsAboutSrcdoc()) { |
| RenderFrameHostImpl* parent = frame_tree_node()->parent(); |
| |
| if (parent) { |
| return parent->GetLastCommittedOrigin(); |
| } else { |
| // The only path for `parent` to be missing for a srcdoc navigation is if |
| // a mainframe renderer executes `location = "about:srcdoc` instead of |
| // embedding an <iframe srcdoc="..."></iframe> element; this is covered by |
| // NavigationBrowserTest.BlockedSrcDoc* tests. While this will result in |
| // an error page, we might still get here via GetURLInfo if the navigation |
| // encounters a COOP header. In that case we return the origin of the |
| // page that executed the script, knowing that the navigation will fail |
| // anyways. |
| return frame_tree_node()->current_frame_host()->GetLastCommittedOrigin(); |
| } |
| } |
| |
| if (GetURL().SchemeIsBlob()) { |
| // Blob URLs either have the origin embedded within the URL, or have a |
| // URL -> origin mapping for it saved in the BlobURLRegistry. |
| std::optional<int> target_rph_id; |
| if (HasRenderFrameHost() && GetRenderFrameHost()->GetProcess()) { |
| target_rph_id = GetRenderFrameHost()->GetProcess()->GetDeprecatedID(); |
| } |
| return static_cast<StoragePartitionImpl*>( |
| GetStoragePartitionWithCurrentSiteInfo()) |
| ->GetBlobUrlRegistry() |
| ->GetOriginForNavigation( |
| GetURL(), common_params().initiator_origin.value_or(url::Origin()), |
| target_rph_id); |
| } |
| |
| // In cases not covered above, URLLoaderFactory should be associated with the |
| // origin of |common_params.url| and/or |common_params.initiator_origin|. |
| url::Origin resolved_origin = url::Origin::Resolve( |
| common_params().url, |
| common_params().initiator_origin.value_or(url::Origin())); |
| |
| if (common_params().url.SchemeIs(url::kDataScheme)) { |
| // Cache the origin for data: URLs, so that its nonce remains stable. |
| tentative_data_origin_to_commit_ = resolved_origin; |
| } |
| |
| return resolved_origin; |
| } |
| |
| bool NavigationRequest::HasLoader() const { |
| return loader_.get() != nullptr; |
| } |
| |
| blink::mojom::PageSwapEventParamsPtr NavigationRequest::WillDispatchPageSwap() { |
| CHECK(ShouldDispatchPageSwapEvent()); |
| |
| did_fire_page_swap_ = true; |
| |
| if (did_encounter_cross_origin_redirect_) { |
| return nullptr; |
| } |
| |
| // The `pageswap` event is fired on the old Document to provide information |
| // about the new Document. The information shared must be restricted to |
| // same-origin Documents. |
| const bool is_same_origin = |
| frame_tree_node_->current_origin().IsSameOriginWith( |
| is_running_potential_prerender_activation_checks_ |
| ? GetTentativeOriginAtRequestTime() |
| : *GetOriginToCommit()); |
| if (!is_same_origin) { |
| return nullptr; |
| } |
| |
| auto page_swap_event_params = blink::mojom::PageSwapEventParams::New(); |
| page_swap_event_params->url = common_params_->url; |
| |
| switch (common_params_->navigation_type) { |
| case blink::mojom::NavigationType::RELOAD: |
| case blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE: |
| page_swap_event_params->navigation_type = |
| blink::mojom::NavigationTypeForNavigationApi::kReload; |
| break; |
| |
| case blink::mojom::NavigationType::RESTORE: |
| case blink::mojom::NavigationType::RESTORE_WITH_POST: |
| // When traversing to a restored entry, we use these navigation types. |
| // Process them same as traverse navigations. |
| case blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT: |
| page_swap_event_params->navigation_type = |
| blink::mojom::NavigationTypeForNavigationApi::kTraverse; |
| page_swap_event_params->page_state = commit_params_->page_state; |
| break; |
| |
| case blink::mojom::NavigationType::DIFFERENT_DOCUMENT: |
| page_swap_event_params->navigation_type = |
| common_params_->should_replace_current_entry |
| ? blink::mojom::NavigationTypeForNavigationApi::kReplace |
| : blink::mojom::NavigationTypeForNavigationApi::kPush; |
| break; |
| |
| case blink::mojom::NavigationType::HISTORY_SAME_DOCUMENT: |
| case blink::mojom::NavigationType::SAME_DOCUMENT: |
| NOTREACHED() << "Same-document navigations shouldn't fire pageswap"; |
| } |
| |
| return page_swap_event_params; |
| } |
| |
| std::optional<NavigationDiscardReason> |
| NavigationRequest::GetNavigationDiscardReason() { |
| return navigation_discard_reason_; |
| } |
| |
| NavigationDiscardReason NavigationRequest::GetTypeForNavigationDiscardReason() { |
| if (NavigationTypeUtils::IsReload(common_params_->navigation_type)) { |
| return NavigationDiscardReason::kNewReloadNavigation; |
| } |
| |
| if (NavigationTypeUtils::IsHistory(common_params_->navigation_type)) { |
| return NavigationDiscardReason::kNewHistoryNavigation; |
| } |
| |
| if (IsRendererInitiated()) { |
| return NavigationDiscardReason::kNewOtherNavigationRendererInitiated; |
| } |
| return NavigationDiscardReason::kNewOtherNavigationBrowserInitiated; |
| } |
| |
| std::optional<ukm::builders::NavigationTimeline> |
| NavigationRequest::GetNavigationTimelineUkmBuilder() { |
| if (ShouldRecordNavigationTimelineUkm() && IsInMainFrame() && |
| // UKM data is sampled at a frequency of `kUkmSamplingRate`. |
| base::RandDouble() < kUkmSamplingRate) { |
| return ukm::builders::NavigationTimeline(GetNextPageUkmSourceId()); |
| } |
| return std::nullopt; |
| } |
| |
| bool NavigationRequest::ShouldRecordNavigationTimelineUkm() const { |
| return !IsSameDocument() && !IsRestore() && |
| !NavigationTypeUtils::IsHistory(common_params_->navigation_type) && |
| !NavigationTypeUtils::IsReload(common_params_->navigation_type) && |
| common_params_->url.SchemeIsHTTPOrHTTPS() && |
| !IsPrerenderedPageActivation(); |
| } |
| |
| void NavigationRequest::MaybeRecordTraceEventsAndHistograms() { |
| if (navigation_handle_timing_.navigation_commit_sent_time.is_null()) { |
| return; |
| } |
| |
| bool record_metrics = ShouldRecordNavigationTimelineUkm(); |
| |
| DCHECK(!blink::IsRendererDebugURL(common_params_->url)); |
| base::TimeTicks navigation_start_time = common_params_->navigation_start; |
| DCHECK(!navigation_start_time.is_null()); |
| const auto trace_id = TRACE_ID_WITH_SCOPE("NavigationBreakdown", |
| TRACE_ID_LOCAL(navigation_id_)); |
| const base::TimeTicks loader_start_time = |
| navigation_handle_timing_.loader_start_time; |
| const base::TimeTicks first_request_start_time = |
| navigation_handle_timing_.first_request_start_time; |
| const base::TimeTicks navigation_commit_sent_time = |
| navigation_handle_timing_.navigation_commit_sent_time; |
| |
| #define MAYBE_RECORD_TRACE_AND_HISTOGRAM0(name, begin_time, end_time) \ |
| do { \ |
| if (!begin_time.is_null() && !end_time.is_null() && \ |
| navigation_start_time <= begin_time && \ |
| end_time <= navigation_commit_sent_time) { \ |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0("navigation", name, \ |
| trace_id, begin_time); \ |
| TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0("navigation", name, \ |
| trace_id, end_time); \ |
| if (record_metrics) { \ |
| base::UmaHistogramTimes( \ |
| "Navigation.MainFrame.NewNavigation.IgnoreRestore." \ |
| "IsHTTPOrHTTPS." name ".Time2", \ |
| end_time - begin_time); \ |
| } \ |
| } \ |
| } while (0) |
| |
| #define MAYBE_RECORD_TRACE_AND_HISTOGRAM1(name, begin_time, end_time, \ |
| arg1_name, arg1_val) \ |
| do { \ |
| if (!begin_time.is_null() && !end_time.is_null() && \ |
| navigation_start_time <= begin_time && \ |
| end_time <= navigation_commit_sent_time) { \ |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1( \ |
| "navigation", name, trace_id, begin_time, arg1_name, arg1_val); \ |
| TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0("navigation", name, \ |
| trace_id, end_time); \ |
| if (record_metrics) { \ |
| base::UmaHistogramTimes( \ |
| "Navigation.MainFrame.NewNavigation.IgnoreRestore." \ |
| "IsHTTPOrHTTPS." name ".Time2", \ |
| end_time - begin_time); \ |
| } \ |
| } \ |
| } while (0) |
| |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("NavigationStartToBeginNavigation", |
| navigation_start_time, |
| begin_navigation_time_); |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("BeginNavigationToLoaderStart", |
| begin_navigation_time_, loader_start_time); |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM1("LoaderStartToReceiveResponse", |
| loader_start_time, receive_response_time_, |
| "URL", common_params_->url.spec()); |
| |
| // `first_fetch_start_time_` can be earlier than |
| // `loader_start_time` when Prefetch or Prerendering |
| // is enabled. The following UMAs are not recorded in such cases because it |
| // will skew the data. Also the following trace events are not recorded in |
| // such cases because such traces will not be rendered correctly. |
| if (loader_start_time <= first_fetch_start_time_) { |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0( |
| "LoaderStartToFetchStart", loader_start_time, first_fetch_start_time_); |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("FetchStart", first_fetch_start_time_, |
| first_request_start_time); |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("ReceiveHeaders", |
| first_request_start_time, |
| final_receive_headers_end_time_); |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("ReceiveHeadersToReceiveResponse", |
| final_receive_headers_end_time_, |
| receive_response_time_); |
| } |
| |
| MAYBE_RECORD_TRACE_AND_HISTOGRAM0("ReceiveResponseToCommitNavigation", |
| receive_response_time_, |
| navigation_commit_sent_time); |
| |
| // UKM data is sampled at a frequency of `kUkmSamplingRate`. |
| if (record_metrics && base::RandDouble() < kUkmSamplingRate && |
| !navigation_start_time.is_null() && !begin_navigation_time_.is_null() && |
| !loader_start_time.is_null() && !receive_response_time_.is_null() && |
| navigation_start_time <= begin_navigation_time_ && |
| begin_navigation_time_ <= loader_start_time && |
| loader_start_time <= receive_response_time_ && |
| receive_response_time_ <= navigation_commit_sent_time) { |
| ukm::builders::NavigationRequestBreakDown ukm(GetNextPageUkmSourceId()); |
| ukm.SetNavigationStartToBeginNavigation( |
| (begin_navigation_time_ - navigation_start_time).InMilliseconds()) |
| .SetBeginNavigationToLoaderStart( |
| (loader_start_time - begin_navigation_time_).InMilliseconds()) |
| .SetLoaderStartToReceiveResponse( |
| (receive_response_time_ - loader_start_time).InMilliseconds()) |
| .SetReceiveResponseToCommitNavigation( |
| (navigation_commit_sent_time - receive_response_time_) |
| .InMilliseconds()); |
| |
| // To avoid affecting other metrics, we check the following conditions |
| // separately. These conditions should usually be true, but there can be |
| // uncommon error cases. |
| if (!first_fetch_start_time_.is_null() && |
| !first_request_start_time.is_null() && |
| loader_start_time <= first_fetch_start_time_ && |
| first_fetch_start_time_ <= first_request_start_time) { |
| ukm.SetLoaderStartToFetchStart( |
| (first_fetch_start_time_ - loader_start_time).InMilliseconds()) |
| .SetFetchStartToRequestStart( |
| (first_request_start_time - first_fetch_start_time_) |
| .InMilliseconds()); |
| } |
| |
| ukm.Record(ukm::UkmRecorder::Get()); |
| } |
| |
| #undef MAYBE_RECORD_TRACE_AND_HISTOGRAM0 |
| #undef MAYBE_RECORD_TRACE_AND_HISTOGRAM1 |
| } |
| |
| void NavigationRequest::MaybeRecordNavigationStartAdjustments() { |
| // Only record metrics if we have a navigation start time. |
| if (common_params().navigation_start.is_null()) { |
| return; |
| } |
| |
| if (IsSameDocument()) { |
| // No adjustments are made for same-document navigations. |
| CHECK(original_navigation_start_.is_null()); |
| return; |
| } |
| |
| // Some navigations do not adjust the start time, in which case |
| // `original_navigation_start_` is left as null. |
| if (original_navigation_start_.is_null()) { |
| base::UmaHistogramEnumeration("Navigation.StartAdjustment.AllFrames", |
| NavigationStartAdjustmentType::kNone); |
| if (IsInPrimaryMainFrame()) { |
| base::UmaHistogramEnumeration("Navigation.StartAdjustment.MainFrameOnly", |
| NavigationStartAdjustmentType::kNone); |
| } |
| return; |
| } |
| |
| // Compute the adjustment made and what percentage of the total navigation |
| // time it includes (approximating now as the end of the navigation). |
| base::TimeDelta adjustment = |
| common_params().navigation_start - original_navigation_start_; |
| base::TimeDelta original_start_to_finish = |
| base::TimeTicks::Now() - original_navigation_start_; |
| // Note that in unit tests, all timestamps can be the same. Skip recording |
| // duration metrics if no time has elapsed during the navigation. |
| if (original_start_to_finish.is_zero()) { |
| return; |
| } |
| |
| // Report the adjustment in UMA metrics specific to the case that occurred. |
| NavigationStartAdjustmentType adjustment_type = |
| NavigationStartAdjustmentType::kNone; |
| std::string histogram_name = "Navigation.StartAdjustment"; |
| if (navigation_start_adjustment_for_legacy_) { |
| // No beforeunload handlers actually run in legacy mode. |
| CHECK(!beforeunload_dialog_shown_); |
| adjustment_type = NavigationStartAdjustmentType::kLegacyPostTask; |
| histogram_name += ".LegacyPostTask"; |
| } else if (!beforeunload_dialog_shown_) { |
| adjustment_type = NavigationStartAdjustmentType::kBeforeUnloadHandlers; |
| histogram_name += ".BeforeUnloadHandlers"; |
| } else { |
| adjustment_type = NavigationStartAdjustmentType::kBeforeUnloadDialog; |
| histogram_name += ".BeforeUnloadDialog"; |
| } |
| base::UmaHistogramEnumeration("Navigation.StartAdjustment.AllFrames", |
| adjustment_type); |
| if (IsInPrimaryMainFrame()) { |
| base::UmaHistogramEnumeration("Navigation.StartAdjustment.MainFrameOnly", |
| adjustment_type); |
| } |
| |
| // It is currently possible for the adjustment to be negative, due to a bug |
| // where the updated start time from an earlier navigation is applied to the |
| // current NavigationRequest. See https://crbug.com/385170155. |
| if (adjustment.is_negative()) { |
| // In this case, report the absolute value of the adjustment in a different |
| // per-type metric, without reporting the (ill-defined) percentage or trying |
| // to create a trace event. |
| histogram_name += ".Negative"; |
| base::UmaHistogramTimes(histogram_name, adjustment.magnitude()); |
| return; |
| } |
| |
| // The duration of the navigation should never be negative. For now, return |
| // early if that happens, after reporting a DumpWithoutCrashing. Upgrade this |
| // to a CHECK failure if no reports are received by M135. |
| if (original_start_to_finish.is_negative()) { |
| NOTREACHED() << original_start_to_finish; |
| } |
| |
| base::UmaHistogramTimes(histogram_name, adjustment); |
| size_t percentage = 100 * adjustment / original_start_to_finish; |
| base::UmaHistogramPercentage(histogram_name + ".Percentage", percentage); |
| |
| // Show trace events indicating where the adjustment occurred in time. |
| const auto trace_id = TRACE_ID_WITH_SCOPE("NavigationStartAdjustment", |
| TRACE_ID_LOCAL(navigation_id_)); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1( |
| "navigation", "NavigationStartAdjustment", trace_id, |
| original_navigation_start_, "Percentage", percentage); |
| TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0( |
| "navigation", "NavigationStartAdjustment", trace_id, |
| common_params().navigation_start); |
| } |
| |
| void NavigationRequest::WillStartBeforeUnload() { |
| SetWaitingForRendererResponse(); |
| beforeunload_phase2_start_time_ = base::TimeTicks().Now(); |
| } |
| |
| NavigationRequest::Timeline::Timeline() = default; |
| NavigationRequest::Timeline::Timeline( |
| const NavigationRequest::Timeline& timeline) = default; |
| NavigationRequest::Timeline::~Timeline() = default; |
| |
| void NavigationRequest::Timeline::MarkFinish() { |
| finish = base::TimeTicks().Now(); |
| } |
| |
| NavigationRequest::Timeline |
| NavigationRequest::GenerateNavigationTimelineForMetrics( |
| const mojom::DidCommitProvisionalLoadParams& params, |
| const base::TimeTicks& did_commit_ipc_received_time) { |
| NavigationRequest::Timeline timeline; |
| |
| if (is_synchronous_renderer_commit()) { |
| // For synchronous renderer commits, the start time in the renderer process |
| // should be provided in `actual_navigation_start`. |
| if (!common_params().actual_navigation_start.is_null()) { |
| timeline.start = common_params().actual_navigation_start; |
| } else { |
| // If `actual_navigation_start` is unexpectedly missing, fall back to the |
| // time this IPC was received. |
| timeline.start = did_commit_ipc_received_time; |
| } |
| } else if (!common_params().actual_navigation_start.is_null()) { |
| // Use the actual start time if it is provided, and record how long was |
| // spent on beforeunload phase 1 in the initiating renderer (if any). |
| timeline.start = common_params().actual_navigation_start; |
| if (!begin_params().before_unload_start.is_null()) { |
| timeline.beforeunload_phase1_start = begin_params().before_unload_start; |
| timeline.beforeunload_phase1_end = begin_params().before_unload_end; |
| } |
| } else { |
| // For any legacy cases where the actual start time isn't provided, fall |
| // back to the `navigation_start` used by web-exposed metrics. However, if |
| // the navigation start time was adjusted due to beforeunload processing, |
| // use the original timestamp to ensure that the trace event start time is |
| // still accurate. |
| // |
| // TODO(alexmos): examine whether these timestamps, as well as their other |
| // uses, should use InterProcessTimeTicksConverter since they may come from |
| // the renderer process. |
| timeline.start = !original_navigation_start_.is_null() |
| ? original_navigation_start_ |
| : common_params().navigation_start; |
| } |
| |
| timeline.navigation_request_creation = creation_time_; |
| if (!beforeunload_phase2_start_time_.is_null()) { |
| timeline.beforeunload_phase2_start = beforeunload_phase2_start_time_; |
| timeline.beforeunload_phase2_end = beforeunload_phase2_end_time_; |
| } |
| timeline.common_params_start = common_params().navigation_start; |
| timeline.begin_navigation = begin_navigation_time_; |
| timeline.loader_start = navigation_handle_timing_.loader_start_time; |
| if (!IsPageActivation()) { |
| // Prerender and bfcache activations should not use the stale loader values |
| // from the original commit. (Note: `loader_start` and `receive_response` |
| // are both set to fresh values for page activations.) |
| timeline.loader_fetch_start = first_fetch_start_time_; |
| timeline.loader_receive_headers = final_receive_headers_end_time_; |
| } |
| timeline.receive_response = receive_response_time_; |
| timeline.commit_ipc_sent = |
| !IsPageActivation() |
| ? navigation_handle_timing_.navigation_commit_sent_time |
| : page_activation_commit_time_; |
| |
| // Note that we can't use NavigationRequest's |
| // navigation_handle_timing_.navigation_commit_received_time because it's |
| // not populated yet when this function is called. |
| if (!IsPageActivation()) { |
| timeline.renderer_commit_ipc_received = params.commit_navigation_start; |
| timeline.renderer_did_commit_ipc_sent = params.commit_reply_sent; |
| timeline.did_commit_ipc_received = did_commit_ipc_received_time; |
| } else { |
| // Page activations don't send a commit IPC, so use a zero size interval. |
| timeline.did_commit_ipc_received = timeline.commit_ipc_sent; |
| } |
| |
| return timeline; |
| } |
| |
| void NavigationRequest::SanitizeDocumentIsolationPolicyHeader() { |
| if (!response_head_) { |
| return; |
| } |
| |
| // If the DocumentIsolationPolicy feature is not enabled, set its |
| // DocumentIsolationPolicy to its default value. |
| if (!base::FeatureList::IsEnabled( |
| network::features::kDocumentIsolationPolicy)) { |
| response_head_->parsed_headers->document_isolation_policy = |
| network::DocumentIsolationPolicy(); |
| return; |
| } |
| |
| // DocumentIsolationPolicy is only supported in strict SiteIsolation mode for |
| // now. Set it to its default value if the platform does not support strict |
| // SiteIsolation. |
| if (!SiteIsolationPolicy::UseDedicatedProcessesForAllSites()) { |
| response_head_->parsed_headers->document_isolation_policy = |
| network::DocumentIsolationPolicy(); |
| return; |
| } |
| |
| network::DocumentIsolationPolicy& dip = |
| response_head_->parsed_headers->document_isolation_policy; |
| bool has_dip_header = |
| dip.value != network::mojom::DocumentIsolationPolicyValue::kNone || |
| dip.report_only_value != |
| network::mojom::DocumentIsolationPolicyValue::kNone || |
| dip.reporting_endpoint || dip.report_only_reporting_endpoint; |
| |
| // DocumentIsolationPolicy should only be used by secure contexts. |
| if (!network::IsUrlPotentiallyTrustworthy(GetURL()) && has_dip_header) { |
| response_head_->parsed_headers->document_isolation_policy = |
| network::DocumentIsolationPolicy(); |
| AddDeferredConsoleMessage( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "The Document-Isolation-Policy header has been ignored because " |
| "the URL's origin was untrustworthy. Please deliver the response using " |
| "the HTTPS protocol. You can also use the 'localhost' origin " |
| "instead. See " |
| "https://www.w3.org/TR/powerful-features/" |
| "#potentially-trustworthy-origin."); |
| } |
| } |
| |
| void NavigationRequest::ValidateCommitOrigin( |
| const url::Origin& origin_to_commit) { |
| NavigationEntryImpl* nav_entry = |
| static_cast<NavigationEntryImpl*>(GetNavigationEntry()); |
| if (!nav_entry) { |
| return; |
| } |
| |
| FrameNavigationEntry* frame_entry = |
| nav_entry->GetFrameEntry(frame_tree_node_); |
| if (!frame_entry || !frame_entry->committed_origin().has_value()) { |
| return; |
| } |
| |
| const url::Origin& expected_origin = frame_entry->committed_origin().value(); |
| bool origins_match = false; |
| // Current weakened check: allows precursor tuple comparison if *either* |
| // origin is opaque. This is a temporary workaround because sandbox |
| // navigations do not currently clear PageState properly. |
| // |
| // TODO(crbug.com/421948889): After this bug is fixed, tighten this check |
| // to only allow precursor tuple comparison if *both* origins are opaque. |
| if (expected_origin.opaque() || origin_to_commit.opaque()) { |
| // Both origins are opaque — compare by their precursor tuples. |
| origins_match = expected_origin.GetTupleOrPrecursorTupleIfOpaque() == |
| origin_to_commit.GetTupleOrPrecursorTupleIfOpaque(); |
| } else { |
| // If either is non-opaque, use regular strict comparison. |
| origins_match = expected_origin.IsSameOriginWith(origin_to_commit); |
| } |
| |
| if (!origins_match) { |
| // TODO(crbug.com/420965165): In redirects or other origin-changing |
| // cases (e.g., CSP), FrameNavigationEntry may retain a stale |
| // committed_origin(). We should clear it when the origin changes, so |
| // that if it exists at commit time, it can be trusted to match. |
| // |
| // In the meantime, it’s safe for a stale committed_origin() to stick |
| // around **only** if there’s no origin-related state (e.g., PageState) |
| // being sent in commit_params_. |
| CHECK(commit_params_->page_state.empty(), base::NotFatalUntil::M140) |
| << "PageState wasn't cleared after a commit origin mismatch." |
| << "expected_origin: " << expected_origin |
| << ", origin_to_commit: " << origin_to_commit; |
| } |
| } |
| |
| } // namespace content |