| // Copyright 2017 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/loader/navigation_url_loader_impl.h" |
| |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/strcat.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/download/public/common/download_stats.h" |
| #include "content/browser/about_url_loader_factory.h" |
| #include "content/browser/attribution_reporting/attribution_manager.h" |
| #include "content/browser/blob_storage/chrome_blob_storage_context.h" |
| #include "content/browser/client_hints/client_hints.h" |
| #include "content/browser/data_url_loader_factory.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/file_system/file_system_url_loader_factory.h" |
| #include "content/browser/loader/file_url_loader_factory.h" |
| #include "content/browser/loader/navigation_early_hints_manager.h" |
| #include "content/browser/loader/navigation_loader_interceptor.h" |
| #include "content/browser/loader/navigation_url_loader_delegate.h" |
| #include "content/browser/loader/response_head_update_params.h" |
| #include "content/browser/loader/subresource_proxying_url_loader_service.h" |
| #include "content/browser/loader/url_loader_factory_utils.h" |
| #include "content/browser/navigation_subresource_loader_params.h" |
| #include "content/browser/preloading/prefetch/prefetch_features.h" |
| #include "content/browser/preloading/prefetch/prefetch_url_loader_interceptor.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/navigation_request_info.h" |
| #include "content/browser/service_worker/service_worker_client.h" |
| #include "content/browser/service_worker/service_worker_main_resource_handle.h" |
| #include "content/browser/service_worker/service_worker_main_resource_loader_interceptor.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/web_package/prefetched_signed_exchange_cache.h" |
| #include "content/browser/web_package/signed_exchange_consts.h" |
| #include "content/browser/web_package/signed_exchange_request_handler.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/browser/webui/url_data_manager_backend.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/client_hints.h" |
| #include "content/public/browser/client_hints_controller_delegate.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/download_utils.h" |
| #include "content/public/browser/frame_accept_header.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/shared_cors_origin_access_list.h" |
| #include "content/public/browser/ssl_status.h" |
| #include "content/public/browser/url_loader_request_interceptor.h" |
| #include "content/public/browser/url_loader_throttles.h" |
| #include "content/public/browser/web_ui_url_loader_factory.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/referrer.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "content/public/common/webplugininfo.h" |
| #include "media/media_buildflags.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/load_timing_info.h" |
| #include "net/cert/sct_status_flags.h" |
| #include "net/cert/signed_certificate_timestamp_and_status.h" |
| #include "net/cookies/cookie_setting_override.h" |
| #include "net/http/http_content_disposition.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/ssl/ssl_info.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "net/url_request/redirect_util.h" |
| #include "ppapi/buildflags/buildflags.h" |
| #include "services/metrics/public/cpp/metrics_utils.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/constants.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/parsed_headers.h" |
| #include "services/network/public/cpp/request_destination.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/cpp/url_loader_factory_builder.h" |
| #include "services/network/public/cpp/url_util.h" |
| #include "services/network/public/cpp/web_sandbox_flags.h" |
| #include "services/network/public/cpp/wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/network_context.mojom-forward.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/loader/mime_sniffing_throttle.h" |
| #include "third_party/blink/public/common/loader/record_load_histograms.h" |
| #include "third_party/blink/public/common/loader/throttling_url_loader.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/service_worker/service_worker_router_rule.mojom.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "content/browser/android/content_url_loader_factory.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PLUGINS) |
| #include "content/public/browser/plugin_service.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| class NavigationLoaderInterceptorBrowserContainer |
| : public NavigationLoaderInterceptor { |
| public: |
| explicit NavigationLoaderInterceptorBrowserContainer( |
| std::unique_ptr<URLLoaderRequestInterceptor> browser_interceptor) |
| : browser_interceptor_(std::move(browser_interceptor)) {} |
| |
| ~NavigationLoaderInterceptorBrowserContainer() override = default; |
| |
| void MaybeCreateLoader( |
| const network::ResourceRequest& tentative_resource_request, |
| BrowserContext* browser_context, |
| LoaderCallback callback, |
| FallbackCallback fallback_callback) override { |
| browser_interceptor_->MaybeCreateLoader( |
| tentative_resource_request, browser_context, |
| base::BindOnce( |
| [](LoaderCallback callback, |
| URLLoaderRequestInterceptor::RequestHandler handler) { |
| if (handler) { |
| std::move(callback).Run(NavigationLoaderInterceptor::Result( |
| base::MakeRefCounted< |
| network::SingleRequestURLLoaderFactory>( |
| std::move(handler)), |
| /*subresource_loader_params=*/{})); |
| } else { |
| std::move(callback).Run(std::nullopt); |
| } |
| }, |
| std::move(callback))); |
| } |
| |
| bool MaybeCreateLoaderForResponse( |
| const network::URLLoaderCompletionStatus& status, |
| const network::ResourceRequest& request, |
| network::mojom::URLResponseHeadPtr* response_head, |
| mojo::ScopedDataPipeConsumerHandle* response_body, |
| mojo::PendingRemote<network::mojom::URLLoader>* loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver, |
| blink::ThrottlingURLLoader* url_loader, |
| bool* skip_other_interceptors) override { |
| return browser_interceptor_->MaybeCreateLoaderForResponse( |
| status, request, response_head, response_body, loader, client_receiver, |
| url_loader); |
| } |
| |
| private: |
| std::unique_ptr<URLLoaderRequestInterceptor> browser_interceptor_; |
| }; |
| |
| class NavigationTimingThrottle : public blink::URLLoaderThrottle { |
| public: |
| NavigationTimingThrottle(bool is_outermost_main_frame, base::TimeTicks start) |
| : is_outermost_main_frame_(is_outermost_main_frame), start_(start) {} |
| |
| void WillStartRequest(network::ResourceRequest* request, |
| bool* defer) override { |
| base::UmaHistogramTimes( |
| base::StrCat({"Navigation.LoaderCreateToRequestStart.", |
| is_outermost_main_frame_ ? "MainFrame" : "Subframe"}), |
| base::TimeTicks::Now() - start_); |
| } |
| |
| private: |
| bool is_outermost_main_frame_; |
| base::TimeTicks start_; |
| }; |
| |
| const net::NetworkTrafficAnnotationTag kNavigationUrlLoaderTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("navigation_url_loader", R"( |
| semantics { |
| sender: "Navigation URL Loader" |
| description: |
| "This request is issued by a main frame navigation to fetch the " |
| "content of the page that is being navigated to." |
| trigger: |
| "Navigating Chrome (by clicking on a link, bookmark, history item, " |
| "using session restore, etc)." |
| data: |
| "Arbitrary site-controlled data can be included in the URL, HTTP " |
| "headers, and request body. Requests may include cookies and " |
| "site-specific credentials." |
| destination: WEBSITE |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: "This feature cannot be disabled." |
| chrome_policy { |
| URLBlocklist { |
| URLBlocklist: { entries: '*' } |
| } |
| } |
| chrome_policy { |
| URLAllowlist { |
| URLAllowlist { } |
| } |
| } |
| } |
| comments: |
| "Chrome would be unable to navigate to websites without this type of " |
| "request. Using either URLBlocklist or URLAllowlist policies (or a " |
| "combination of both) limits the scope of these requests." |
| )"); |
| |
| std::unique_ptr<network::ResourceRequest> CreateResourceRequest( |
| const NavigationRequestInfo& request_info, |
| FrameTreeNode* frame_tree_node, |
| mojo::PendingRemote<network::mojom::CookieAccessObserver> cookie_observer, |
| mojo::PendingRemote<network::mojom::TrustTokenAccessObserver> |
| trust_token_observer, |
| mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver> |
| shared_dictionary_observer, |
| mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver> |
| url_loader_network_observer, |
| mojo::PendingRemote<network::mojom::DevToolsObserver> devtools_observer, |
| mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver> |
| device_bound_session_observer, |
| mojo::PendingRemote<network::mojom::AcceptCHFrameObserver> |
| accept_ch_frame_observer) { |
| auto new_request = CreateResourceRequestForNavigation( |
| request_info.common_params->method, request_info.common_params->url, |
| request_info.common_params->request_destination, |
| *request_info.common_params->referrer, request_info.isolation_info, |
| std::move(devtools_observer), /*priority=*/net::HIGHEST, |
| request_info.is_main_frame); |
| |
| new_request->trusted_params->cookie_observer = std::move(cookie_observer); |
| new_request->trusted_params->trust_token_observer = |
| std::move(trust_token_observer); |
| new_request->trusted_params->shared_dictionary_observer = |
| std::move(shared_dictionary_observer); |
| new_request->trusted_params->url_loader_network_observer = |
| std::move(url_loader_network_observer); |
| new_request->trusted_params->device_bound_session_observer = |
| std::move(device_bound_session_observer); |
| new_request->trusted_params->client_security_state = |
| request_info.client_security_state.Clone(); |
| new_request->trusted_params->accept_ch_frame_observer = |
| std::move(accept_ch_frame_observer); |
| new_request->trusted_params->allow_cookies_from_browser = |
| request_info.allow_cookies_from_browser; |
| new_request->is_outermost_main_frame = request_info.is_outermost_main_frame; |
| new_request->request_initiator = request_info.common_params->initiator_origin; |
| new_request->headers.AddHeadersFromString(request_info.begin_params->headers); |
| new_request->cors_exempt_headers = request_info.cors_exempt_headers; |
| new_request->devtools_accepted_stream_types = |
| request_info.devtools_accepted_stream_types; |
| // For ResourceType purposes, fenced frames are considered a kSubFrame. |
| new_request->resource_type = |
| static_cast<int>(request_info.is_outermost_main_frame |
| ? blink::mojom::ResourceType::kMainFrame |
| : blink::mojom::ResourceType::kSubFrame); |
| |
| int load_flags = request_info.begin_params->load_flags; |
| if (request_info.is_outermost_main_frame) { |
| load_flags |= net::LOAD_MAIN_FRAME_DEPRECATED; |
| load_flags |= net::LOAD_CAN_USE_RESTRICTED_PREFETCH_FOR_MAIN_FRAME; |
| } |
| |
| // Sync loads should have maximum priority and should be the only |
| // requests that have the ignore limits flag set. |
| DCHECK(!(load_flags & net::LOAD_IGNORE_LIMITS)); |
| |
| new_request->load_flags = load_flags; |
| |
| new_request->request_body = request_info.common_params->post_data.get(); |
| new_request->has_user_gesture = request_info.common_params->has_user_gesture; |
| |
| if (ui::PageTransitionIsWebTriggerable( |
| ui::PageTransitionFromInt(request_info.common_params->transition))) { |
| new_request->trusted_params->has_user_activation = |
| request_info.common_params->has_user_gesture; |
| } else { |
| new_request->trusted_params->has_user_activation = true; |
| } |
| |
| new_request->upgrade_if_insecure = request_info.upgrade_if_insecure; |
| new_request->throttling_profile_id = request_info.devtools_frame_token; |
| new_request->transition_type = request_info.common_params->transition; |
| devtools_instrumentation::MaybeAssignResourceRequestId( |
| frame_tree_node, request_info.devtools_navigation_token.ToString(), |
| *new_request); |
| if (request_info.begin_params->trust_token_params) { |
| new_request->trust_token_params = |
| *request_info.begin_params->trust_token_params; |
| } |
| |
| new_request->storage_access_api_status = |
| request_info.begin_params->storage_access_api_status; |
| |
| WebContentsImpl* web_contents = static_cast<WebContentsImpl*>( |
| WebContents::FromFrameTreeNodeId(frame_tree_node->frame_tree_node_id())); |
| new_request->attribution_reporting_support = |
| web_contents ? web_contents->GetAttributionSupport() |
| : AttributionManager::GetAttributionSupport( |
| /*client_os_disabled=*/false); |
| |
| new_request->attribution_reporting_eligibility = |
| request_info.begin_params->impression.has_value() |
| ? network::mojom::AttributionReportingEligibility::kNavigationSource |
| : network::mojom::AttributionReportingEligibility::kUnset; |
| |
| new_request->shared_storage_writable_eligible = |
| request_info.shared_storage_writable_eligible; |
| new_request->is_ad_tagged = request_info.is_ad_tagged; |
| |
| return new_request; |
| } |
| |
| // Called for requests that we don't have a URLLoaderFactory for. |
| void UnknownSchemeCallback( |
| bool handled_externally, |
| const network::ResourceRequest& /* resource_request */, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| mojo::Remote<network::mojom::URLLoaderClient>(std::move(client)) |
| ->OnComplete(network::URLLoaderCompletionStatus( |
| handled_externally ? net::ERR_ABORTED : net::ERR_UNKNOWN_URL_SCHEME)); |
| } |
| |
| void LogQueueTimeHistogram(std::string_view name, |
| bool is_outermost_main_frame) { |
| auto* task = base::TaskAnnotator::CurrentTaskForThread(); |
| // Only log for non-delayed tasks with a valid queue_time. |
| if (!task || task->queue_time.is_null() || |
| !task->delayed_run_time.is_null()) { |
| return; |
| } |
| |
| base::UmaHistogramTimes( |
| base::StrCat( |
| {name, is_outermost_main_frame ? ".MainFrame" : ".Subframe"}), |
| base::TimeTicks::Now() - task->queue_time); |
| } |
| |
| void LogAcceptCHFrameStatus(AcceptCHFrameRestart status) { |
| base::UmaHistogramEnumeration("ClientHints.AcceptCHFrame", status); |
| } |
| |
| bool IsSameOriginRedirect(const std::vector<GURL>& url_chain) { |
| if (url_chain.size() < 2) { |
| return false; |
| } |
| |
| auto previous_origin = url::Origin::Create(url_chain[url_chain.size() - 2]); |
| return previous_origin.IsSameOriginWith(url_chain[url_chain.size() - 1]); |
| } |
| |
| // Return whether the inherited frame policy or iframe sandbox attribute |
| // contains the 'allow-same-site-none-cookies' value and the override should be |
| // applied as the frame's ancestors are all same-site. |
| bool ShouldAllowSameSiteNoneCookiesInSandbox(FrameTreeNode& frame_tree_node) { |
| if (frame_tree_node.IsInFencedFrameTree()) { |
| return false; |
| } |
| |
| RenderFrameHostImpl* frame = frame_tree_node.current_frame_host(); |
| return frame && |
| frame->active_sandbox_flags() != |
| network::mojom::WebSandboxFlags::kNone && |
| !frame->IsSandboxed( |
| network::mojom::WebSandboxFlags::kAllowSameSiteNoneCookies) && |
| frame->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin) && |
| frame->AncestorsAllowSameSiteNoneCookiesOverride( |
| frame_tree_node.navigation_request() |
| ->GetTentativeOriginAtRequestTime()); |
| } |
| |
| // TODO(https://crbug.com/346000235) there is a known failure with extensions |
| // See test: ExtensionWebRequestApiTestWithContextType.HSTSUpgradeAfterRedirect |
| // We still want to check this in debug mode to avoid further regressions, but |
| // because it was affecting to many developers with DCHECK_IS_ON() this was |
| // downgraded as a DEBUG only check. |
| // After getting the approval to rewrite the CSP parser in Rust, we should be |
| // able to remove "pre-parsing" of CSP in the network process and directly |
| // parse them later in the browser process. This will make this check to become |
| // unnecessary. |
| #ifndef NDEBUG |
| void CheckParsedHeadersEquals(const network::mojom::ParsedHeadersPtr& lhs, |
| const network::mojom::ParsedHeadersPtr& rhs, |
| const GURL& url) { |
| // If we're running this function it means we're re-parsing the headers from |
| // cache and checking if they equal the prior parsing results. As the |
| // Clear-Site-Data header isn't cached we want to be sure not to fail just |
| // because the two parsing results mismatch in this expected way. |
| network::mojom::ParsedHeadersPtr adjusted_lhs = lhs->Clone(); |
| if (rhs->client_hints_ignored_due_to_clear_site_data_header) { |
| CHECK(!rhs->accept_ch); |
| CHECK(!rhs->critical_ch); |
| adjusted_lhs->accept_ch = std::nullopt; |
| adjusted_lhs->critical_ch = std::nullopt; |
| adjusted_lhs->client_hints_ignored_due_to_clear_site_data_header = true; |
| } |
| if (mojo::Equals(adjusted_lhs, rhs)) { |
| return; |
| } |
| |
| // TODO(crbug.com/40864513) Remove this instrumentation once fixed. |
| auto to_string = [](const auto& policies) { |
| std::string out; |
| for (const auto& csp : policies) { |
| out += csp->header->header_value + " | "; |
| } |
| return out; |
| }; |
| SCOPED_CRASH_KEY_STRING256("bug1362779", "csp_adjusted_lhs", |
| to_string(adjusted_lhs->content_security_policy)); |
| SCOPED_CRASH_KEY_STRING256("bug1362779", "csp_rhs", |
| to_string(rhs->content_security_policy)); |
| SCOPED_CRASH_KEY_STRING32("bug1362779", "url", url.possibly_invalid_spec()); |
| |
| CHECK(mojo::Equals(adjusted_lhs->content_security_policy, |
| rhs->content_security_policy)); |
| CHECK(mojo::Equals(adjusted_lhs->allow_csp_from, rhs->allow_csp_from)); |
| CHECK(mojo::Equals(adjusted_lhs->cross_origin_embedder_policy, |
| rhs->cross_origin_embedder_policy)); |
| CHECK(mojo::Equals(adjusted_lhs->cross_origin_opener_policy, |
| rhs->cross_origin_opener_policy)); |
| CHECK(mojo::Equals(adjusted_lhs->document_isolation_policy, |
| rhs->document_isolation_policy)); |
| CHECK(mojo::Equals(adjusted_lhs->origin_agent_cluster, |
| rhs->origin_agent_cluster)); |
| CHECK(mojo::Equals(adjusted_lhs->accept_ch, rhs->accept_ch)); |
| CHECK(mojo::Equals(adjusted_lhs->critical_ch, rhs->critical_ch)); |
| CHECK_EQ(adjusted_lhs->xfo, rhs->xfo); |
| CHECK(mojo::Equals(adjusted_lhs->link_headers, rhs->link_headers)); |
| CHECK(mojo::Equals(adjusted_lhs->timing_allow_origin, |
| rhs->timing_allow_origin)); |
| CHECK(mojo::Equals(adjusted_lhs->supports_loading_mode, |
| rhs->supports_loading_mode)); |
| CHECK(mojo::Equals(adjusted_lhs->reporting_endpoints, |
| rhs->reporting_endpoints)); |
| CHECK(mojo::Equals(adjusted_lhs->cookie_indices, rhs->cookie_indices)); |
| CHECK(mojo::Equals(adjusted_lhs->avail_language, rhs->avail_language)); |
| CHECK(mojo::Equals(adjusted_lhs->content_language, rhs->content_language)); |
| CHECK(mojo::Equals(adjusted_lhs->no_vary_search_with_parse_error, |
| rhs->no_vary_search_with_parse_error)); |
| CHECK(mojo::Equals(adjusted_lhs->observe_browsing_topics, |
| rhs->observe_browsing_topics)); |
| CHECK(mojo::Equals(adjusted_lhs->allow_cross_origin_event_reporting, |
| rhs->allow_cross_origin_event_reporting)); |
| NOTREACHED() << "The parsed headers don't match, but we don't know which " |
| "field does not match. Please add a DCHECK before this one " |
| "checking for the missing field."; |
| } |
| #endif // NDEBUG |
| |
| } // namespace |
| |
| std::unique_ptr<network::ResourceRequest> CreateResourceRequestForNavigation( |
| const std::string& method, |
| const GURL& url, |
| network::mojom::RequestDestination destination, |
| const blink::mojom::Referrer& referrer, |
| const net::IsolationInfo& isolation_info, |
| mojo::PendingRemote<network::mojom::DevToolsObserver> devtools_observer, |
| net::RequestPriority priority, |
| bool is_main_frame) { |
| auto new_request = std::make_unique<network::ResourceRequest>(); |
| |
| // - Step 3 of |
| // https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching |
| // - Step 2 of |
| // https://wicg.github.io/nav-speculation/prefetch.html#create-a-navigation-request |
| |
| // url: entry's URL [spec text] |
| new_request->url = url; |
| new_request->navigation_redirect_chain.push_back(new_request->url); |
| |
| // client: sourceSnapshotParams's fetch client [spec text] |
| |
| // destination: "document" [spec text] |
| new_request->destination = destination; |
| |
| // credentials mode: "include" [spec text] |
| new_request->credentials_mode = network::mojom::CredentialsMode::kInclude; |
| |
| // use-URL-credentials flag: set [spec text] |
| |
| // redirect mode: "manual" [spec text] |
| new_request->redirect_mode = network::mojom::RedirectMode::kManual; |
| |
| // replaces client id: navigable's active document's relevant settings |
| // object's id [spec text] |
| // Not implemented (https://crbug.com/40287592). |
| |
| // mode: "navigate" [spec text] |
| new_request->mode = network::mojom::RequestMode::kNavigate; |
| |
| // referrer: entry's document state's request referrer [spec text] |
| new_request->referrer = referrer.url; |
| |
| // referrer policy: entry's document state's request referrer policy [spec |
| // text] |
| new_request->referrer_policy = |
| Referrer::ReferrerPolicyForUrlRequest(referrer.policy); |
| |
| new_request->method = method; |
| |
| new_request->site_for_cookies = isolation_info.site_for_cookies(); |
| |
| new_request->trusted_params = network::ResourceRequest::TrustedParams(); |
| new_request->trusted_params->isolation_info = isolation_info; |
| new_request->trusted_params->devtools_observer = std::move(devtools_observer); |
| |
| new_request->priority = priority; |
| |
| if (is_main_frame) { |
| // When set, `update_first_party_url_on_redirect` will cause a |
| // server-redirect to update the URL used to determine if cookies are |
| // first-party. Since fenced frames are main frames in terms of cookie |
| // partitioning, this needs to be `is_main_frame` rather than |
| // `is_outermost_main_frame`. |
| new_request->update_first_party_url_on_redirect = true; |
| |
| // Navigation responses for the top-level document are able to be used as |
| // compression dictionaries. |
| new_request->shared_dictionary_writer_enabled = true; |
| } |
| |
| new_request->enable_load_timing = true; |
| |
| if (base::FeatureList::IsEnabled( |
| network::features::kRendererSideContentDecoding)) { |
| new_request->client_side_content_decoding_enabled = true; |
| } |
| |
| return new_request; |
| } |
| |
| // TODO(kinuko): Fix the method ordering and move these methods after the ctor. |
| NavigationURLLoaderImpl::~NavigationURLLoaderImpl() { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationURLLoaderImpl::~NavigationURLLoaderImpl", |
| TRACE_ID_LOCAL(this), TRACE_EVENT_FLAG_FLOW_IN); |
| // If neither OnCompleted nor OnReceivedResponse has been invoked, the |
| // request was canceled before receiving a response, so log a cancellation. |
| // Results after receiving a non-error response are logged in the renderer, |
| // if the request is passed to one. If it's a download, or not passed to a |
| // renderer for some other reason, results will not be logged for the |
| // request. The net::OK check may not be necessary - the case where OK is |
| // received without receiving any headers looks broken, anyways. |
| if (!received_response_ && (!status_ || status_->error_code != net::OK)) { |
| blink::RecordLoadHistograms( |
| url::Origin::Create(url_), resource_request_->destination, |
| status_ ? status_->error_code : net::ERR_ABORTED); |
| } |
| } |
| |
| void NavigationURLLoaderImpl::Start() { |
| TRACE_EVENT_WITH_FLOW0("navigation", "NavigationURLLoaderImpl::Start", |
| TRACE_ID_LOCAL(this), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(!started_); |
| started_ = true; |
| |
| resource_request_->headers.SetHeader( |
| net::HttpRequestHeaders::kAccept, |
| FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, browser_context_)); |
| |
| // If not performing a PDF navigation, allow certain schemes to create loaders |
| // directly, bypassing interceptors. (In the case of PDF navigation, |
| // interception is required, but these loaders are not; see crbug.com/1253314 |
| // and crbug.com/1253984.) |
| // |
| // TODO(crbug.com/40794764): Consider getting rid of these exceptions. |
| if (!request_info_->is_pdf) { |
| // Requests to WebUI scheme won't get redirected to/from other schemes |
| // or be intercepted, so we just let it go here. |
| std::string scheme = request_info_->common_params->url.scheme(); |
| if (base::Contains(URLDataManagerBackend::GetWebUISchemes(), scheme)) { |
| FrameTreeNode* frame_tree_node = |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_); |
| CHECK(frame_tree_node); |
| CHECK(frame_tree_node->navigation_request()); |
| |
| CreateThrottlingLoaderAndStart( |
| url_loader_factory::Create( |
| ContentBrowserClient::URLLoaderFactoryType::kNavigation, |
| url_loader_factory::TerminalParams::ForNonNetwork( |
| CreateWebUIURLLoaderFactory( |
| frame_tree_node->current_frame_host(), scheme, {}), |
| network::mojom::kBrowserProcessId), |
| url_loader_factory::ContentClientParams( |
| browser_context_, frame_tree_node->current_frame_host(), |
| frame_tree_node->current_frame_host() |
| ->GetProcess() |
| ->GetDeprecatedID(), |
| resource_request_->request_initiator.value_or(url::Origin()), |
| net::IsolationInfo(), |
| ukm::SourceIdObj::FromInt64(ukm_source_id_), |
| /*bypass_redirect_checks=*/nullptr, |
| frame_tree_node->navigation_request()->GetNavigationId(), |
| GetUIThreadTaskRunner( |
| {BrowserTaskType::kNavigationNetworkResponse}))), |
| /*additional_throttles=*/{}); |
| return; |
| } |
| |
| // Requests to Blob scheme won't get redirected to/from other schemes or be |
| // intercepted, so we just let it go here. |
| if (request_info_->common_params->url.SchemeIsBlob() && |
| request_info_->blob_url_loader_factory) { |
| CreateThrottlingLoaderAndStart( |
| network::SharedURLLoaderFactory::Create( |
| std::move(request_info_->blob_url_loader_factory)), |
| /*additional_throttles=*/{}); |
| return; |
| } |
| } |
| |
| CreateInterceptors(); |
| Restart(); |
| } |
| |
| void NavigationURLLoaderImpl::CreateInterceptors() { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationURLLoaderImpl::CreateInterceptors", |
| TRACE_ID_LOCAL(this), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| if (prefetched_signed_exchange_cache_) { |
| std::unique_ptr<NavigationLoaderInterceptor> |
| prefetched_signed_exchange_interceptor = |
| prefetched_signed_exchange_cache_->MaybeCreateInterceptor( |
| url_, frame_tree_node_id_, |
| resource_request_->trusted_params->isolation_info); |
| prefetched_signed_exchange_cache_.reset(); |
| if (prefetched_signed_exchange_interceptor) { |
| interceptors_.push_back( |
| std::move(prefetched_signed_exchange_interceptor)); |
| } |
| } |
| |
| // Set up an interceptor for service workers. |
| if (service_worker_handle_) { |
| auto service_worker_interceptor = |
| ServiceWorkerMainResourceLoaderInterceptor::CreateForNavigation( |
| resource_request_->url, service_worker_handle_->AsWeakPtr(), |
| *request_info_); |
| // The interceptor may not be created in certain cases (e.g., the origin |
| // is not secure). |
| if (service_worker_interceptor) { |
| if (base::FeatureList::IsEnabled(features::kPrefetchServiceWorker)) { |
| // Set up an interceptor for ServiceWorker-controlled prefetches. This |
| // is needed before the ServiceWorkerMainResourceLoaderInterceptor which |
| // would also intercept the request for ServiceWorker-controlled URLs. |
| // See the design docs at https://crbug.com/40947546. |
| interceptors_.push_back(std::make_unique<PrefetchURLLoaderInterceptor>( |
| PrefetchServiceWorkerState::kControlled, |
| service_worker_handle_->AsWeakPtr(), frame_tree_node_id_, |
| request_info_->initiator_document_token, |
| request_info_->prefetch_serving_page_metrics_container)); |
| } |
| |
| interceptors_.push_back(std::move(service_worker_interceptor)); |
| } |
| } |
| |
| // Set-up an interceptor for SignedExchange handling if it is enabled. |
| if (signed_exchange_utils::IsSignedExchangeHandlingEnabled( |
| browser_context_)) { |
| interceptors_.push_back(CreateSignedExchangeRequestHandler( |
| *request_info_, network_loader_factory_)); |
| } |
| |
| // Set up an interceptor for prefetch. |
| // When `features::kPrefetchServiceWorker` is enabled, we intentionally add |
| // two `PrefetchURLLoaderInterceptor`s, one for ServiceWorker-controlled |
| // prefetches above, and one for non-ServiceWorker-controlled prefetches here. |
| // See the design docs at https://crbug.com/40947546. |
| interceptors_.push_back(std::make_unique<PrefetchURLLoaderInterceptor>( |
| PrefetchServiceWorkerState::kDisallowed, |
| /*service_worker_handle=*/nullptr, frame_tree_node_id_, |
| request_info_->initiator_document_token, |
| request_info_->prefetch_serving_page_metrics_container)); |
| |
| // See if embedders want to add interceptors. |
| std::vector<std::unique_ptr<URLLoaderRequestInterceptor>> |
| browser_interceptors = |
| GetContentClient()->browser()->WillCreateURLLoaderRequestInterceptors( |
| navigation_ui_data_.get(), frame_tree_node_id_, |
| request_info_->navigation_id, |
| request_info_->force_no_https_upgrade, |
| GetUIThreadTaskRunner( |
| {BrowserTaskType::kNavigationNetworkResponse})); |
| if (!browser_interceptors.empty()) { |
| for (auto& browser_interceptor : browser_interceptors) { |
| interceptors_.push_back( |
| std::make_unique<NavigationLoaderInterceptorBrowserContainer>( |
| std::move(browser_interceptor))); |
| } |
| } |
| } |
| |
| void NavigationURLLoaderImpl::Restart() { |
| // Cancel all inflight early hints preloads except for same origin redirects. |
| if (!IsSameOriginRedirect(resource_request_->navigation_redirect_chain)) { |
| early_hints_manager_.reset(); |
| } |
| |
| // Clear `url_loader_` if it's not the default one (network). This allows |
| // the restarted request to use a new loader, instead of, e.g., reusing the |
| // service worker loader. For an optimization, we keep and reuse |
| // the default url loader if the all `interceptors_` doesn't handle the |
| // redirected request. If the network service is enabled, reset the loader |
| // if the redirected URL's scheme and the previous URL scheme don't match in |
| // their use or disuse of the network service loader. |
| if (!default_loader_used_ || |
| (resource_request_->navigation_redirect_chain.size() > 1 && |
| network::IsURLHandledByNetworkService( |
| resource_request_->navigation_redirect_chain |
| [resource_request_->navigation_redirect_chain.size() - 1]) != |
| network::IsURLHandledByNetworkService( |
| resource_request_->navigation_redirect_chain |
| [resource_request_->navigation_redirect_chain.size() - |
| 2]))) { |
| if (url_loader_) { |
| url_loader_->ResetForFollowRedirect( |
| *resource_request_.get(), url_loader_removed_headers_, |
| url_loader_modified_headers_, |
| url_loader_modified_cors_exempt_headers_); |
| url_loader_removed_headers_.clear(); |
| url_loader_modified_headers_.Clear(); |
| url_loader_modified_cors_exempt_headers_.Clear(); |
| } |
| url_loader_.reset(); |
| } |
| received_response_ = false; |
| head_update_params_ = ResponseHeadUpdateParams(); |
| MaybeStartLoader(/*next_interceptor_index=*/0, |
| /*interceptor_result=*/std::nullopt); |
| } |
| |
| void NavigationURLLoaderImpl::MaybeStartLoader( |
| size_t next_interceptor_index, |
| std::optional<NavigationLoaderInterceptor::Result> interceptor_result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(started_); |
| |
| if (interceptor_result) { |
| subresource_loader_params_ = |
| std::move(interceptor_result->subresource_loader_params); |
| if (!interceptor_result->single_request_factory) { |
| // Skip the subsequent interceptors and start with the default behavior. |
| // |
| // Here `subresource_loader_params_` can still have non-default values |
| // e.g. when there's a controlling service worker that doesn't have a |
| // fetch event handler so it doesn't intercept requests. |
| StartNonInterceptedRequest( |
| std::move(interceptor_result->response_head_update_params)); |
| return; |
| } |
| |
| // Intercept the request with `interceptor_result->single_request_factory`. |
| StartInterceptedRequest( |
| std::move(interceptor_result->single_request_factory)); |
| return; |
| } |
| |
| subresource_loader_params_ = {}; |
| |
| if (next_interceptor_index >= interceptors_.size()) { |
| // All interceptors have been checked and none has elected to handle the |
| // request. Start with the default behavior. |
| StartNonInterceptedRequest(ResponseHeadUpdateParams()); |
| return; |
| } |
| |
| // Fallback to the next interceptor. |
| auto* next_interceptor = interceptors_[next_interceptor_index].get(); |
| next_interceptor->MaybeCreateLoader( |
| *resource_request_, browser_context_, |
| base::BindOnce(&NavigationURLLoaderImpl::MaybeStartLoader, |
| weak_factory_.GetWeakPtr(), next_interceptor_index + 1), |
| base::BindOnce(&NavigationURLLoaderImpl::FallbackToNonInterceptedRequest, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void NavigationURLLoaderImpl::StartInterceptedRequest( |
| scoped_refptr<network::SharedURLLoaderFactory> single_request_factory) { |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> additional_throttles; |
| // Intercepted requests need MimeSniffingThrottle to do mime sniffing. |
| // Non-intercepted requests usually go through the regular network |
| // URLLoader, which does mime sniffing. |
| additional_throttles.push_back(std::make_unique<blink::MimeSniffingThrottle>( |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse}))); |
| |
| default_loader_used_ = false; |
| // If `url_loader_` already exists, this means we are following a redirect |
| // using an interceptor. In this case we should make sure to reset the |
| // loader, similar to what is done in Restart(). |
| if (url_loader_) { |
| url_loader_->ResetForFollowRedirect( |
| *resource_request_.get(), url_loader_removed_headers_, |
| url_loader_modified_headers_, url_loader_modified_cors_exempt_headers_); |
| url_loader_removed_headers_.clear(); |
| url_loader_modified_headers_.Clear(); |
| url_loader_modified_cors_exempt_headers_.Clear(); |
| url_loader_.reset(); |
| } |
| |
| CreateThrottlingLoaderAndStart(std::move(single_request_factory), |
| std::move(additional_throttles)); |
| } |
| |
| void NavigationURLLoaderImpl::StartNonInterceptedRequest( |
| ResponseHeadUpdateParams head_update_params) { |
| // If we already have the default `url_loader_` we must come here after a |
| // redirect. No interceptors wanted to intercept the redirected request, so |
| // let the loader just follow the redirect. |
| if (url_loader_) { |
| DCHECK(!redirect_info_.new_url.is_empty()); |
| url_loader_->FollowRedirect( |
| std::move(url_loader_removed_headers_), |
| std::move(url_loader_modified_headers_), |
| std::move(url_loader_modified_cors_exempt_headers_)); |
| return; |
| } |
| |
| head_update_params_ = std::move(head_update_params); |
| scoped_refptr<network::SharedURLLoaderFactory> factory; |
| if (network::IsURLHandledByNetworkService(resource_request_->url)) { |
| factory = network_loader_factory_; |
| default_loader_used_ = true; |
| } else { |
| factory = GetOrCreateNonNetworkLoaderFactory(); |
| } |
| |
| response_loader_receiver_.reset(); |
| CreateThrottlingLoaderAndStart(std::move(factory), |
| /*additional_throttles=*/{}); |
| } |
| |
| network::mojom::URLLoaderFactory* |
| NavigationURLLoaderImpl::FallbackToNonInterceptedRequest( |
| base::WeakPtr<NavigationURLLoaderImpl> self, |
| ResponseHeadUpdateParams head_update_params) { |
| if (!self) { |
| return nullptr; |
| } |
| |
| self->head_update_params_ = std::move(head_update_params); |
| if (network::IsURLHandledByNetworkService(self->resource_request_->url)) { |
| // `NavigationURLLoaderImpl::default_loader_used_` is NOT set to true here, |
| // because the underlying URLLoaderFactory of |
| // `NavigationURLLoaderImpl::url_loader_` is still ServiceWorker-provided |
| // one (that finally delegates to `network_loader_factory_` though) and thus |
| // isn't e.g. unsafe to reuse after redirects. |
| return self->network_loader_factory_.get(); |
| } else { |
| return self->GetOrCreateNonNetworkLoaderFactory().get(); |
| } |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> |
| NavigationURLLoaderImpl::GetOrCreateNonNetworkLoaderFactory() { |
| scoped_refptr<network::SharedURLLoaderFactory>& cached_factory = |
| non_network_url_loader_factories_[resource_request_->url.scheme()]; |
| |
| if (cached_factory) { |
| return cached_factory; |
| } |
| |
| auto [is_cacheable, factory] = CreateNonNetworkLoaderFactory( |
| browser_context_, storage_partition_, |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_), |
| ukm::SourceIdObj::FromInt64(ukm_source_id_), navigation_ui_data_.get(), |
| *request_info_, web_contents_getter_, *resource_request_); |
| |
| if (is_cacheable) { |
| cached_factory = factory; |
| } |
| |
| return factory; |
| } |
| |
| // static |
| std::pair</*is_cacheable=*/bool, scoped_refptr<network::SharedURLLoaderFactory>> |
| NavigationURLLoaderImpl::CreateNonNetworkLoaderFactory( |
| BrowserContext* browser_context, |
| StoragePartitionImpl* storage_partition, |
| FrameTreeNode* frame_tree_node, |
| const ukm::SourceIdObj& ukm_id, |
| NavigationUIData* navigation_ui_data, |
| const NavigationRequestInfo& request_info, |
| base::RepeatingCallback<WebContents*()> web_contents_getter, |
| const network::ResourceRequest& resource_request) { |
| CHECK(frame_tree_node); |
| CHECK(frame_tree_node->navigation_request()); |
| |
| // First, check known schemes. |
| if (mojo::PendingRemote<network::mojom::URLLoaderFactory> terminal = |
| CreateTerminalNonNetworkLoaderFactory( |
| browser_context, storage_partition, frame_tree_node, |
| resource_request.url)) { |
| auto* frame = frame_tree_node->current_frame_host(); |
| |
| // TODO(lukasza, jam): It is unclear why FileURLLoaderFactory is the only |
| // non-http factory that allows DevTools interception. For comparison all |
| // non-WebUI cases in RFHI::CommitNavigation allow DevTools interception. |
| // Let's try to be more consistent / less ad-hoc. |
| std::optional<devtools_instrumentation::WillCreateURLLoaderFactoryParams> |
| devtools_params = |
| resource_request.url.SchemeIs(url::kFileScheme) |
| ? std::make_optional( |
| devtools_instrumentation:: |
| WillCreateURLLoaderFactoryParams::ForFrame(frame)) |
| : std::nullopt; |
| return std::make_pair( |
| /*is_cacheable=*/true, |
| url_loader_factory::Create( |
| ContentBrowserClient::URLLoaderFactoryType::kNavigation, |
| url_loader_factory::TerminalParams::ForNonNetwork( |
| std::move(terminal), network::mojom::kBrowserProcessId), |
| url_loader_factory::ContentClientParams( |
| frame->GetSiteInstance()->GetBrowserContext(), frame, |
| frame->GetProcess()->GetDeprecatedID(), url::Origin(), |
| net::IsolationInfo(), ukm_id, |
| /*bypass_redirect_checks=*/nullptr, |
| frame_tree_node->navigation_request()->GetNavigationId(), |
| GetUIThreadTaskRunner( |
| {BrowserTaskType::kNavigationNetworkResponse})), |
| devtools_params)); |
| } |
| |
| // Second, check external protocols. |
| std::optional<url::Origin> initiating_origin; |
| if (resource_request.navigation_redirect_chain.size() > 1) { |
| // The last URL in `navigation_redirect_chain` is an external-protocol URL |
| // (if handled by `HandleExternalProtocol`), and the second-to-last URL is |
| // the URL that initiated the redirect to the external-protocol URL. |
| initiating_origin = url::Origin::Create( |
| resource_request.navigation_redirect_chain |
| [resource_request.navigation_redirect_chain.size() - 2]); |
| } else { |
| initiating_origin = resource_request.request_initiator; |
| } |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| terminal_external_protocol; |
| bool handled = GetContentClient()->browser()->HandleExternalProtocol( |
| resource_request.url, std::move(web_contents_getter), |
| frame_tree_node->frame_tree_node_id(), navigation_ui_data, |
| request_info.is_primary_main_frame, |
| frame_tree_node->IsInFencedFrameTree(), request_info.sandbox_flags, |
| static_cast<ui::PageTransition>(resource_request.transition_type), |
| resource_request.has_user_gesture, initiating_origin, |
| request_info.initiator_document_token |
| ? RenderFrameHostImpl::FromDocumentToken( |
| request_info.initiator_process_id, |
| *request_info.initiator_document_token) |
| : nullptr, |
| request_info.isolation_info, &terminal_external_protocol); |
| if (terminal_external_protocol) { |
| return std::make_pair( |
| /*is_cacheable=*/false, |
| url_loader_factory::Create( |
| ContentBrowserClient::URLLoaderFactoryType::kNavigation, |
| url_loader_factory::TerminalParams::ForNonNetwork( |
| std::move(terminal_external_protocol), |
| network::mojom::kBrowserProcessId))); |
| } |
| |
| // Finally handle as an unknown scheme. |
| return std::make_pair( |
| /*is_cacheable=*/false, |
| url_loader_factory::Create( |
| ContentBrowserClient::URLLoaderFactoryType::kNavigation, |
| url_loader_factory::TerminalParams::ForNonNetwork( |
| base::MakeRefCounted<network::SingleRequestURLLoaderFactory>( |
| base::BindOnce(UnknownSchemeCallback, handled)), |
| network::mojom::kBrowserProcessId))); |
| } |
| |
| void NavigationURLLoaderImpl::CreateThrottlingLoaderAndStart( |
| scoped_refptr<network::SharedURLLoaderFactory> factory, |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> |
| additional_throttles) { |
| TRACE_EVENT_WITH_FLOW0( |
| "navigation", "NavigationURLLoaderImpl::CreateThrottlingLoaderAndStart", |
| TRACE_ID_LOCAL(this), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| CHECK(!url_loader_); |
| |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles = |
| CreateURLLoaderThrottles(); |
| for (auto&& throttle : additional_throttles) { |
| throttles.push_back(std::move(throttle)); |
| } |
| |
| uint32_t options = |
| GetURLLoaderOptions(resource_request_->is_outermost_main_frame); |
| |
| url_loader_ = blink::ThrottlingURLLoader::CreateLoaderAndStart( |
| std::move(factory), std::move(throttles), global_request_id_.request_id, |
| options, resource_request_.get(), /*client=*/this, |
| kNavigationUrlLoaderTrafficAnnotation, |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| } |
| |
| void NavigationURLLoaderImpl::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| // Early Hints should not come after actual response. |
| DCHECK(!received_response_); |
| DCHECK_NE(early_hints->ip_address_space, |
| network::mojom::IPAddressSpace::kUnknown); |
| |
| // Ignore Early Hints for embed and object destination. |
| if (request_info_->common_params->request_destination == |
| network::mojom::RequestDestination::kEmbed || |
| request_info_->common_params->request_destination == |
| network::mojom::RequestDestination::kObject) { |
| return; |
| } |
| |
| FrameTreeNode* frame_tree_node = |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_); |
| |
| // Allow Early Hints preload only for outermost main frames. Calculating |
| // appropriate parameters to create URLLoaderFactory for subframes and fenced |
| // frames are complicated and not supported yet. |
| if (frame_tree_node->GetParentOrOuterDocument()) { |
| return; |
| } |
| |
| if (!early_hints_manager_) { |
| std::optional<NavigationEarlyHintsManagerParams> params = |
| delegate_->CreateNavigationEarlyHintsManagerParams(*early_hints); |
| if (!params) { |
| return; |
| } |
| early_hints_manager_ = std::make_unique<NavigationEarlyHintsManager>( |
| *browser_context_, *storage_partition_, frame_tree_node_id_, |
| std::move(*params)); |
| } |
| |
| early_hints_manager_->HandleEarlyHints(std::move(early_hints), |
| *resource_request_.get()); |
| } |
| |
| void NavigationURLLoaderImpl::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr head, |
| mojo::ScopedDataPipeConsumerHandle response_body, |
| std::optional<mojo_base::BigBuffer> cached_metadata) { |
| DCHECK(!cached_metadata); |
| LogQueueTimeHistogram("Navigation.QueueTime.OnReceiveResponse", |
| resource_request_->is_outermost_main_frame); |
| |
| // Early Hints preloads should not be committed for PDF. |
| // See https://github.com/whatwg/html/issues/7823 |
| if (head->mime_type == "application/pdf" || head->mime_type == "text/pdf") { |
| early_hints_manager_.reset(); |
| } |
| |
| // TODO(crbug.com/40496584):Resolved an issue where creating RPHI would cause |
| // a crash when the browser context was shut down. We are actively exploring |
| // the appropriate long-term solution. Please remove this condition once the |
| // final fix is implemented. |
| if (browser_context_->ShutdownStarted()) { |
| return; |
| } |
| |
| if (!response_body) { |
| return; |
| } |
| |
| response_body_ = std::move(response_body); |
| received_response_ = true; |
| |
| if (!head_update_params_.load_timing_info.service_worker_start_time |
| .is_null()) { |
| head->load_timing.service_worker_start_time = |
| head_update_params_.load_timing_info.service_worker_start_time; |
| head->load_timing.service_worker_ready_time = |
| head_update_params_.load_timing_info.service_worker_ready_time; |
| } |
| if (head_update_params_.initial_service_worker_status.has_value()) { |
| head->initial_service_worker_status = |
| head_update_params_.initial_service_worker_status; |
| } |
| if (!head_update_params_.router_info.is_null()) { |
| head->service_worker_router_info = |
| std::move(head_update_params_.router_info); |
| } |
| if (!head_update_params_.load_timing_info |
| .service_worker_router_evaluation_start.is_null()) { |
| head->load_timing.service_worker_router_evaluation_start = |
| head_update_params_.load_timing_info |
| .service_worker_router_evaluation_start; |
| } |
| |
| // If the default loader (network) was used to handle the URL load request |
| // we need to see if the interceptors want to potentially create a new |
| // loader for the response. e.g. service workers. |
| // |
| // As the navigation request has received a response, the URLLoader has |
| // completed without any network errors. Some interceptors may still wish |
| // to handle the response. |
| auto status = network::URLLoaderCompletionStatus(net::OK); |
| if (MaybeCreateLoaderForResponse(status, &head)) { |
| return; |
| } |
| |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints; |
| |
| if (url_loader_) { |
| url_loader_client_endpoints = url_loader_->Unbind(); |
| } else { |
| url_loader_client_endpoints = network::mojom::URLLoaderClientEndpoints::New( |
| std::move(response_url_loader_), response_loader_receiver_.Unbind()); |
| } |
| |
| // 304 responses should abort the navigation, rather than display the page. |
| // This needs to be after the URLLoader has been moved to |
| // `url_loader_client_endpoints` in order to abort the request, to avoid |
| // receiving unexpected call. |
| if (head->headers && |
| head->headers->response_code() == net::HTTP_NOT_MODIFIED) { |
| // Call CancelWithError instead of OnComplete so that if there is an |
| // intercepting URLLoaderFactory it gets notified. |
| url_loader_->CancelWithError( |
| net::ERR_ABORTED, |
| std::string_view(base::NumberToString(net::ERR_ABORTED))); |
| return; |
| } |
| |
| bool must_download = download_utils::MustDownload( |
| browser_context_, url_, head->headers.get(), head->mime_type); |
| bool known_mime_type = blink::IsSupportedMimeType(head->mime_type); |
| |
| #if BUILDFLAG(ENABLE_PLUGINS) |
| if (!head->intercepted_by_plugin && !must_download && !known_mime_type) { |
| // No plugin throttles intercepted the response. Ask if the plugin |
| // registered to PluginService wants to handle the request. |
| CheckPluginAndContinueOnReceiveResponse( |
| std::move(head), std::move(url_loader_client_endpoints), |
| /*is_download_if_not_handled_by_plugin=*/true, |
| std::vector<WebPluginInfo>()); |
| return; |
| } |
| #endif |
| |
| // When a plugin intercepted the response, we don't want to download it. |
| bool is_download = |
| !head->intercepted_by_plugin && (must_download || !known_mime_type); |
| |
| CallOnReceivedResponse(std::move(head), |
| std::move(url_loader_client_endpoints), is_download); |
| } |
| |
| #if BUILDFLAG(ENABLE_PLUGINS) |
| void NavigationURLLoaderImpl::CheckPluginAndContinueOnReceiveResponse( |
| network::mojom::URLResponseHeadPtr head, |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| bool is_download_if_not_handled_by_plugin, |
| const std::vector<WebPluginInfo>& plugins) { |
| bool stale; |
| WebPluginInfo plugin; |
| bool has_plugin = PluginService::GetInstance()->GetPluginInfo( |
| browser_context_, resource_request_->url, head->mime_type, |
| /*allow_wildcard=*/false, &stale, &plugin, nullptr); |
| |
| if (stale) { |
| // Refresh the plugins asynchronously. |
| PluginService::GetInstance()->GetPlugins(base::BindOnce( |
| &NavigationURLLoaderImpl::CheckPluginAndContinueOnReceiveResponse, |
| weak_factory_.GetWeakPtr(), std::move(head), |
| std::move(url_loader_client_endpoints), |
| is_download_if_not_handled_by_plugin)); |
| return; |
| } |
| |
| bool is_download = !has_plugin && is_download_if_not_handled_by_plugin; |
| CallOnReceivedResponse(std::move(head), |
| std::move(url_loader_client_endpoints), is_download); |
| } |
| #endif |
| |
| void NavigationURLLoaderImpl::CallOnReceivedResponse( |
| network::mojom::URLResponseHeadPtr head, |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| bool is_download) { |
| // Record navigation loader response metrics. We don't want to record the |
| // metrics for requests that had redirects to avoid adding noise to the |
| // latency measurements. |
| if (resource_request_->is_outermost_main_frame && |
| resource_request_->navigation_redirect_chain.size() == 1) { |
| RecordReceivedResponseUkmForOutermostMainFrame(); |
| } |
| |
| network::mojom::URLResponseHead* head_ptr = head.get(); |
| |
| // Record ServiceWorker and the Static Routing API metrics. |
| MaybeRecordServiceWorkerMainResourceInfo(head); |
| |
| auto on_receive_response = base::BindOnce( |
| &NavigationURLLoaderImpl::NotifyResponseStarted, |
| weak_factory_.GetWeakPtr(), std::move(head), |
| std::move(url_loader_client_endpoints), std::move(response_body_), |
| global_request_id_, is_download); |
| |
| ParseHeaders(url_, head_ptr, std::move(on_receive_response)); |
| } |
| |
| void NavigationURLLoaderImpl::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head) { |
| LogQueueTimeHistogram("Navigation.QueueTime.OnReceiveRedirect", |
| resource_request_->is_outermost_main_frame); |
| net::Error error = net::OK; |
| if (!bypass_redirect_checks_ && |
| !IsSafeRedirectTarget(url_, redirect_info.new_url)) { |
| error = net::ERR_UNSAFE_REDIRECT; |
| } else if (--redirect_limit_ == 0) { |
| error = net::ERR_TOO_MANY_REDIRECTS; |
| if (redirect_info.is_signed_exchange_fallback_redirect) { |
| UMA_HISTOGRAM_BOOLEAN("SignedExchange.FallbackRedirectLoop", true); |
| } |
| } |
| if (error != net::OK) { |
| if (url_loader_) { |
| // Call CancelWithError instead of OnComplete so that if there is an |
| // intercepting URLLoaderFactory (created through the embedder's |
| // ContentBrowserClient::WillCreateURLLoaderFactory) it gets notified. |
| url_loader_->CancelWithError( |
| error, std::string_view(base::NumberToString(error))); |
| } else { |
| // TODO(crbug.com/40118809): Make sure ResetWithReason() is called |
| // on the original `url_loader_`. |
| OnComplete(network::URLLoaderCompletionStatus(error)); |
| } |
| return; |
| } |
| |
| // Store the redirect_info for later use in FollowRedirect where we give |
| // our interceptors_ a chance to intercept the request for the new location. |
| redirect_info_ = redirect_info; |
| |
| GURL previous_url = url_; |
| url_ = redirect_info.new_url; |
| |
| network::mojom::URLResponseHead* head_ptr = head.get(); |
| auto on_receive_redirect = base::BindOnce( |
| &NavigationURLLoaderImpl::NotifyRequestRedirected, |
| weak_factory_.GetWeakPtr(), redirect_info, std::move(head)); |
| ParseHeaders(previous_url, head_ptr, std::move(on_receive_redirect)); |
| } |
| |
| void NavigationURLLoaderImpl::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback callback) { |
| NOTREACHED(); |
| } |
| |
| void NavigationURLLoaderImpl::OnTransferSizeUpdated( |
| int32_t transfer_size_diff) { |
| network::RecordOnTransferSizeUpdatedUMA( |
| network::OnTransferSizeUpdatedFrom::kNavigationURLLoaderImpl); |
| } |
| |
| void NavigationURLLoaderImpl::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| // Successful load must have used OnResponseStarted first. In this case, the |
| // URLLoaderClient has already been transferred to the renderer process and |
| // OnComplete is not expected to be called here. |
| if (status.error_code == net::OK) { |
| SCOPED_CRASH_KEY_STRING256("NavigationURLLoader_Complete", "url", |
| url_.spec()); |
| base::debug::DumpWithoutCrashing(); |
| return; |
| } |
| |
| // If the default loader (network) was used to handle the URL load request |
| // we need to see if the interceptors want to potentially create a new |
| // loader for the response. e.g. service worker. |
| // |
| // Note: Despite having received a response, the HTTP_NOT_MODIFIED(304) ones |
| // are ignored using OnComplete(net::ERR_ABORTED). No interceptor must |
| // be used in this case. |
| if (!received_response_) { |
| auto response = network::mojom::URLResponseHead::New(); |
| if (MaybeCreateLoaderForResponse(status, &response)) { |
| return; |
| } |
| } |
| |
| status_ = status; |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&NavigationURLLoaderImpl::NotifyRequestFailed, |
| weak_factory_.GetWeakPtr(), status)); |
| } |
| |
| void NavigationURLLoaderImpl::OnAcceptCHFrameReceived( |
| const url::Origin& origin, |
| const std::vector<network::mojom::WebClientHintsType>& accept_ch_frame, |
| OnAcceptCHFrameReceivedCallback callback) { |
| received_accept_ch_frame_ = true; |
| if (!base::FeatureList::IsEnabled(network::features::kAcceptCHFrame)) { |
| std::move(callback).Run(net::OK); |
| return; |
| } |
| |
| LogAcceptCHFrameStatus(AcceptCHFrameRestart::kFramePresent); |
| |
| // Given that this is happening in the middle of navigation, there should |
| // always be an owning frame tree node |
| FrameTreeNode* frame_tree_node = |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_); |
| DCHECK(frame_tree_node); |
| // Log each hint requested via an ACCEPT_CH Frame whether or not this caused |
| // the connection to be restarted. |
| auto* ukm_recorder = ukm::UkmRecorder::Get(); |
| for (const auto& hint : accept_ch_frame) { |
| ukm::builders::ClientHints_AcceptCHFrameUsage(ukm_source_id_) |
| .SetType(static_cast<int64_t>(hint)) |
| .Record(ukm_recorder->Get()); |
| } |
| |
| ClientHintsControllerDelegate* client_hint_delegate = |
| browser_context_->GetClientHintsControllerDelegate(); |
| |
| if (!client_hint_delegate) { |
| std::move(callback).Run(net::OK); |
| return; |
| } |
| |
| // Filter out hints that are disabled by features and the like. |
| blink::EnabledClientHints filtered_enabled_hints; |
| for (const auto& hint : accept_ch_frame) { |
| filtered_enabled_hints.SetIsEnabled(hint, true); |
| } |
| const std::vector<network::mojom::WebClientHintsType>& filtered_hints = |
| filtered_enabled_hints.GetEnabledHints(); |
| |
| if (!AreCriticalHintsMissing(origin, frame_tree_node, client_hint_delegate, |
| filtered_hints)) { |
| std::move(callback).Run(net::OK); |
| return; |
| } |
| |
| net::HttpRequestHeaders modified_headers; |
| client_hint_delegate->SetAdditionalClientHints(filtered_hints); |
| AddNavigationRequestClientHintsHeaders( |
| origin, &modified_headers, browser_context_, client_hint_delegate, |
| frame_tree_node->navigation_request()->is_overriding_user_agent(), |
| frame_tree_node, |
| frame_tree_node->navigation_request() |
| ->commit_params() |
| .frame_policy.container_policy); |
| client_hint_delegate->ClearAdditionalClientHints(); |
| |
| LogAcceptCHFrameStatus(AcceptCHFrameRestart::kNavigationRestarted); |
| |
| // Only restart if new headers are actually added. Given that header values |
| // can be changed via the navigation interceptors or previous restarts, the |
| // header values are ignored and only the presence of header names are |
| // checked. |
| bool restart = false; |
| net::HttpRequestHeaders::Iterator header_iter(modified_headers); |
| while (header_iter.GetNext()) { |
| if (!resource_request_->headers.HasHeader(header_iter.name())) { |
| restart = true; |
| break; |
| } |
| } |
| |
| if (!restart) { |
| std::move(callback).Run(net::OK); |
| return; |
| } |
| |
| // While not a true redirect, a redirect loop can be simulated by repeatedly |
| // closing the socket and presenting a different ALPS setting with each new |
| // handshake. |
| if (--accept_ch_restart_limit_ == 0) { |
| LogAcceptCHFrameStatus(AcceptCHFrameRestart::kRedirectOverflow); |
| OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_TOO_MANY_ACCEPT_CH_RESTARTS)); |
| std::move(callback).Run(net::ERR_TOO_MANY_ACCEPT_CH_RESTARTS); |
| return; |
| } |
| |
| std::move(callback).Run(net::ERR_ABORTED); |
| |
| // If the request is restarted, all of the client hints should be replaced |
| // the "original"/non-edited values. |
| resource_request_->headers.MergeFrom(modified_headers); |
| url_loader_.reset(); |
| Restart(); |
| } |
| |
| void NavigationURLLoaderImpl::Clone( |
| mojo::PendingReceiver<network::mojom::AcceptCHFrameObserver> listener) { |
| // Use |kNavigationNetworkResponse| thread runner. Messages received related |
| // to AcceptCHFrame are not order dependent and can restart the navigation, |
| // blocking navigation when they do. |
| accept_ch_frame_observers_.Add( |
| this, std::move(listener), |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| } |
| |
| // Returns true if an interceptor wants to handle the response, i.e. return a |
| // different response, e.g. service workers. |
| bool NavigationURLLoaderImpl::MaybeCreateLoaderForResponse( |
| const network::URLLoaderCompletionStatus& status, |
| network::mojom::URLResponseHeadPtr* response) { |
| if (!default_loader_used_) { |
| return false; |
| } |
| for (auto& interceptor : interceptors_) { |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> |
| response_client_receiver; |
| bool skip_other_interceptors = false; |
| if (interceptor->MaybeCreateLoaderForResponse( |
| status, *resource_request_, response, &response_body_, |
| &response_url_loader_, &response_client_receiver, url_loader_.get(), |
| &skip_other_interceptors)) { |
| response_loader_receiver_.reset(); |
| response_loader_receiver_.Bind( |
| std::move(response_client_receiver), |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| default_loader_used_ = false; |
| url_loader_.reset(); // Consumed above. |
| response_body_.reset(); // Consumed above. |
| if (skip_other_interceptors) { |
| std::vector<std::unique_ptr<NavigationLoaderInterceptor>> |
| new_interceptors; |
| new_interceptors.push_back(std::move(interceptor)); |
| new_interceptors.swap(interceptors_); |
| // Reset the state of ServiceWorkerClient. |
| // Currently we don't support Service Worker in Signed Exchange |
| // pages. The page will not be controlled by service workers. And |
| // Service Worker related APIs will fail with NoDocumentURL error. |
| // TODO(https://crbug/898733): Support SignedExchange loading and |
| // Service Worker integration. Properly populate all params below, and |
| // storage key in particular, when we want to support it. |
| if (service_worker_handle_) { |
| base::WeakPtr<ServiceWorkerClient> service_worker_client = |
| service_worker_handle_->service_worker_client(); |
| if (service_worker_client) { |
| service_worker_client->SetControllerRegistration( |
| nullptr, /*notify_controllerchange=*/false); |
| service_worker_client->UpdateUrls(GURL(), std::nullopt, |
| blink::StorageKey()); |
| } |
| } |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> |
| NavigationURLLoaderImpl::CreateURLLoaderThrottles() { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationURLLoaderImpl::CreateURLLoaderThrottles", |
| TRACE_ID_LOCAL(this), |
| TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT); |
| auto throttles = CreateContentBrowserURLLoaderThrottles( |
| *resource_request_, browser_context_, web_contents_getter_, |
| navigation_ui_data_.get(), frame_tree_node_id_, |
| request_info_->navigation_id); |
| throttles.push_back(std::make_unique<NavigationTimingThrottle>( |
| resource_request_->is_outermost_main_frame, loader_creation_time_)); |
| return throttles; |
| } |
| |
| std::unique_ptr<SignedExchangeRequestHandler> |
| NavigationURLLoaderImpl::CreateSignedExchangeRequestHandler( |
| const NavigationRequestInfo& request_info, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { |
| // It is safe to pass the callback of CreateURLLoaderThrottles with the |
| // unretained `this`, because the passed callback will be used by a |
| // SignedExchangeHandler which is indirectly owned by `this` until its |
| // header is verified and parsed, that's where the getter is used. |
| return std::make_unique<SignedExchangeRequestHandler>( |
| GetURLLoaderOptions(request_info.is_outermost_main_frame), |
| request_info.frame_tree_node_id, request_info.devtools_navigation_token, |
| std::move(url_loader_factory), |
| base::BindRepeating(&NavigationURLLoaderImpl::CreateURLLoaderThrottles, |
| base::Unretained(this)), |
| GetContentClient()->browser()->GetAcceptLangs(browser_context_)); |
| } |
| |
| void NavigationURLLoaderImpl::ParseHeaders( |
| const GURL& url, |
| network::mojom::URLResponseHead* head, |
| base::OnceClosure continuation) { |
| // As an optimization, when we know the parsed headers will be empty, we can |
| // skip the network process roundtrip. |
| // TODO(arthursonzogni): If there are any performance issues, consider |
| // checking the `head->headers` contains at least one header to be parsed. |
| if (!head->headers) { |
| head->parsed_headers = network::mojom::ParsedHeaders::New(); |
| } |
| |
| // If the network service is running in process, skip unnecessary thread hops. |
| if (IsInProcessNetworkService() && !head->parsed_headers) { |
| base::ScopedUmaHistogramTimer in_process( |
| "Navigation.URLLoader.ParseHeaders.InProcessTime", |
| base::ScopedUmaHistogramTimer::ScopedHistogramTiming:: |
| kMicrosecondTimes); |
| head->parsed_headers = |
| network::PopulateParsedHeaders(head->headers.get(), url); |
| } |
| |
| // The main path: |
| // -------------- |
| // The ParsedHeaders are already provided. No more work needed. |
| // |
| // Currently used when the response is coming from: |
| // - Network |
| // - ServiceWorker |
| // - WebUI |
| base::UmaHistogramBoolean("Navigation.URLLoader.InMainPath", |
| static_cast<bool>(head->parsed_headers)); |
| if (head->parsed_headers) { |
| #ifndef NDEBUG |
| // In debug mode, force reparsing the headers and check that they match. |
| auto check = [](base::OnceClosure continuation, |
| network::mojom::URLResponseHead* head, GURL url, |
| base::TimeTicks call_time, |
| network::mojom::ParsedHeadersPtr parsed_headers) { |
| base::UmaHistogramMicrosecondsTimes( |
| "Navigation.URLLoader.ParseHeaders.RoundTripTimeForVerify", |
| base::TimeTicks::Now() - call_time); |
| CheckParsedHeadersEquals(parsed_headers, head->parsed_headers, url); |
| std::move(continuation).Run(); |
| }; |
| GetNetworkService()->ParseHeaders( |
| url, head->headers, |
| base::BindOnce(check, std::move(continuation), head, url, |
| base::TimeTicks::Now())); |
| #else // NDEBUG |
| std::move(continuation).Run(); |
| #endif // NDEBUG |
| return; |
| } |
| |
| auto assign = [](base::OnceClosure continuation, |
| network::mojom::URLResponseHead* head, |
| base::TimeTicks call_time, |
| network::mojom::ParsedHeadersPtr parsed_headers) { |
| base::UmaHistogramMicrosecondsTimes( |
| "Navigation.URLLoader.ParseHeaders.RoundTripTimeForNonNetworkResponse", |
| base::TimeTicks::Now() - call_time); |
| head->parsed_headers = std::move(parsed_headers); |
| std::move(continuation).Run(); |
| }; |
| |
| GetNetworkService()->ParseHeaders( |
| url, head->headers, |
| base::BindOnce(assign, std::move(continuation), head, |
| base::TimeTicks::Now())); |
| } |
| |
| // TODO(crbug.com/40552600): pass `navigation_ui_data` along with the |
| // request so that it could be modified. |
| NavigationURLLoaderImpl::NavigationURLLoaderImpl( |
| BrowserContext* browser_context, |
| StoragePartition* storage_partition, |
| std::unique_ptr<NavigationRequestInfo> request_info, |
| std::unique_ptr<NavigationUIData> navigation_ui_data, |
| ServiceWorkerMainResourceHandle* service_worker_handle, |
| scoped_refptr<PrefetchedSignedExchangeCache> |
| prefetched_signed_exchange_cache, |
| NavigationURLLoaderDelegate* delegate, |
| mojo::PendingRemote<network::mojom::CookieAccessObserver> cookie_observer, |
| mojo::PendingRemote<network::mojom::TrustTokenAccessObserver> |
| trust_token_observer, |
| mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver> |
| shared_dictionary_observer, |
| mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver> |
| url_loader_network_observer, |
| mojo::PendingRemote<network::mojom::DevToolsObserver> devtools_observer, |
| mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver> |
| device_bound_session_observer, |
| std::vector<std::unique_ptr<NavigationLoaderInterceptor>> |
| initial_interceptors) |
| : delegate_(delegate), |
| browser_context_(browser_context), |
| storage_partition_(static_cast<StoragePartitionImpl*>(storage_partition)), |
| service_worker_handle_(service_worker_handle), |
| request_info_(std::move(request_info)), |
| url_(request_info_->common_params->url), |
| frame_tree_node_id_(request_info_->frame_tree_node_id), |
| global_request_id_(GlobalRequestID::MakeBrowserInitiated()), |
| web_contents_getter_( |
| base::BindRepeating(&WebContents::FromFrameTreeNodeId, |
| frame_tree_node_id_)), |
| navigation_ui_data_(std::move(navigation_ui_data)), |
| interceptors_(std::move(initial_interceptors)), |
| prefetched_signed_exchange_cache_( |
| std::move(prefetched_signed_exchange_cache)), |
| loader_creation_time_(base::TimeTicks::Now()), |
| ukm_source_id_(FrameTreeNode::GloballyFindByID(frame_tree_node_id_) |
| ->navigation_request() |
| ->GetNextPageUkmSourceId()) { |
| TRACE_EVENT_WITH_FLOW0("navigation", |
| "NavigationURLLoaderImpl::NavigationURLLoaderImpl", |
| TRACE_ID_LOCAL(this), TRACE_EVENT_FLAG_FLOW_OUT); |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1( |
| "navigation", "Navigation timeToResponseStarted", TRACE_ID_LOCAL(this), |
| request_info_->common_params->navigation_start, "FrameTreeNode id", |
| frame_tree_node_id_); |
| |
| mojo::PendingRemote<network::mojom::AcceptCHFrameObserver> |
| accept_ch_frame_observer; |
| // Use |kNavigationNetworkResponse| thread runner. Messages received related |
| // to AcceptCHFrame are not order dependent and can restart the navigation, |
| // blocking navigation when they do. |
| accept_ch_frame_observers_.Add( |
| this, accept_ch_frame_observer.InitWithNewPipeAndPassReceiver(), |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| |
| FrameTreeNode* frame_tree_node = |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_); |
| DCHECK(frame_tree_node); |
| DCHECK(frame_tree_node->navigation_request()); |
| |
| resource_request_ = CreateResourceRequest( |
| *request_info_, frame_tree_node, std::move(cookie_observer), |
| std::move(trust_token_observer), std::move(shared_dictionary_observer), |
| std::move(url_loader_network_observer), std::move(devtools_observer), |
| std::move(device_bound_session_observer), |
| std::move(accept_ch_frame_observer)); |
| |
| network_loader_factory_ = CreateNetworkLoaderFactory( |
| browser_context_, storage_partition_, frame_tree_node, |
| ukm::SourceIdObj::FromInt64(ukm_source_id_), &bypass_redirect_checks_); |
| } |
| |
| // static |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| NavigationURLLoaderImpl::CreateTerminalNonNetworkLoaderFactory( |
| BrowserContext* browser_context, |
| StoragePartitionImpl* storage_partition, |
| FrameTreeNode* frame_tree_node, |
| const GURL& url) { |
| // Use `ContentBrowserClient`-supplied factory if non-null, to allow the |
| // embedder to override the default factories below. |
| if (auto factory_from_client = |
| GetContentClient() |
| ->browser() |
| ->CreateNonNetworkNavigationURLLoaderFactory( |
| url.scheme(), frame_tree_node->frame_tree_node_id())) { |
| return factory_from_client; |
| } |
| |
| if (url.scheme() == url::kFileSystemScheme) { |
| bool is_nav_allowed = |
| base::FeatureList::IsEnabled( |
| blink::features::kFileSystemUrlNavigationForChromeAppsOnly) && |
| GetContentClient()->browser()->IsFileSystemURLNavigationAllowed( |
| storage_partition->browser_context(), url); |
| if (is_nav_allowed || |
| base::FeatureList::IsEnabled( |
| blink::features::kFileSystemUrlNavigation) || |
| !frame_tree_node->navigation_request()->IsRendererInitiated()) { |
| // TODO(crbug.com/40323778): Once DevTools has support for |
| // sandboxed file system inspection there isn't much reason anymore to |
| // support browser initiated filesystem: navigations, so remove this |
| // entirely at that point. |
| |
| // Navigations in to filesystem: URLs are deprecated entirely for |
| // renderer-initiated navigations except for those explicitly allowed by |
| // the embedder. The logic below is appropriate for browser-initiated |
| // navigations, but it is incorrect to always use first-party |
| // StorageKeys for renderer-initiated navigations when third party |
| // storage partitioning is enabled. |
| const std::string storage_domain; |
| return CreateFileSystemURLLoaderFactory( |
| ChildProcessHost::kInvalidUniqueID, |
| frame_tree_node->frame_tree_node_id(), |
| storage_partition->GetFileSystemContext(), storage_domain, |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(url))); |
| } |
| |
| return {}; |
| } |
| |
| if (url.scheme() == url::kAboutScheme) { |
| return AboutURLLoaderFactory::Create(); |
| } |
| if (url.scheme() == url::kDataScheme) { |
| return DataURLLoaderFactory::Create(); |
| } |
| |
| if (url.scheme() == url::kFileScheme) { |
| // USER_BLOCKING because this scenario is exactly one of the examples |
| // given by the doc comment for USER_BLOCKING: |
| // Loading and rendering a web page after the user clicks a link. |
| base::TaskPriority file_factory_priority = |
| base::TaskPriority::USER_BLOCKING; |
| return FileURLLoaderFactory::Create( |
| browser_context->GetPath(), |
| browser_context->GetSharedCorsOriginAccessList(), |
| file_factory_priority); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (url.scheme() == url::kContentScheme) { |
| return ContentURLLoaderFactory::Create(); |
| } |
| #endif |
| |
| return {}; |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> |
| NavigationURLLoaderImpl::CreateNetworkLoaderFactory( |
| BrowserContext* browser_context, |
| StoragePartitionImpl* storage_partition, |
| FrameTreeNode* frame_tree_node, |
| const ukm::SourceIdObj& ukm_id, |
| bool* bypass_redirect_checks) { |
| mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient> |
| header_client; |
| |
| // The embedder may want to proxy all network-bound URLLoaderFactory |
| // receivers that it can. If it elects to do so, those proxies will be |
| // connected when loader is created if the request type supports proxying. |
| network::URLLoaderFactoryBuilder factory_builder; |
| // Here we give nullptr for `factory_override`, because CORS is no-op for |
| // navigations. |
| GetContentClient()->browser()->WillCreateURLLoaderFactory( |
| browser_context, frame_tree_node->current_frame_host(), |
| frame_tree_node->current_frame_host()->GetProcess()->GetDeprecatedID(), |
| ContentBrowserClient::URLLoaderFactoryType::kNavigation, url::Origin(), |
| net::IsolationInfo(), |
| frame_tree_node->navigation_request()->GetNavigationId(), ukm_id, |
| factory_builder, &header_client, bypass_redirect_checks, |
| /*disable_secure_dns=*/nullptr, /*factory_override=*/nullptr, |
| GetUIThreadTaskRunner({BrowserTaskType::kNavigationNetworkResponse})); |
| |
| auto devtools_params = |
| devtools_instrumentation::WillCreateURLLoaderFactoryParams::ForFrame( |
| frame_tree_node->current_frame_host()); |
| devtools_params.Run(/*is_navigation=*/true, |
| /*is_download=*/false, factory_builder, |
| /*factory_override=*/nullptr); |
| net::CookieSettingOverrides devtools_cookie_overrides; |
| devtools_instrumentation::ApplyNetworkCookieControlsOverrides( |
| devtools_params.agent_host(), devtools_cookie_overrides); |
| |
| net::CookieSettingOverrides cookie_overrides; |
| if (ShouldAllowSameSiteNoneCookiesInSandbox(*frame_tree_node)) { |
| // Include a CookieSettingOverride in the UrlLoaderFactoryParams for the |
| // frame's SharedURLLoaderFactory if the frame contains the |
| // `allow-same-site-none-cookies` value in its sandbox policy. |
| cookie_overrides.Put( |
| net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox); |
| } |
| |
| if (header_client) { |
| return base::MakeRefCounted<network::WrapperSharedURLLoaderFactory>( |
| CreateURLLoaderFactoryWithHeaderClient( |
| std::move(header_client), std::move(factory_builder), |
| storage_partition, std::move(devtools_cookie_overrides), |
| std::move(cookie_overrides))); |
| } else { |
| if (!devtools_cookie_overrides.empty() || !cookie_overrides.empty()) { |
| network::mojom::URLLoaderFactoryParamsPtr params = |
| storage_partition->CreateURLLoaderFactoryParams(); |
| params->devtools_cookie_setting_overrides = |
| std::move(devtools_cookie_overrides); |
| params->cookie_setting_overrides = std::move(cookie_overrides); |
| return std::move(factory_builder) |
| .Finish(storage_partition->GetNetworkContext(), std::move(params)); |
| } |
| return std::move(factory_builder) |
| .Finish(storage_partition->GetURLLoaderFactoryForBrowserProcess()); |
| } |
| } |
| |
| void NavigationURLLoaderImpl::FollowRedirect( |
| const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const net::HttpRequestHeaders& modified_cors_exempt_headers) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(!redirect_info_.new_url.is_empty()); |
| |
| // Update `resource_request_` and call Restart to give our `interceptors_` a |
| // chance at handling the new location. If no interceptor wants to take |
| // over, we'll use the existing url_loader to follow the redirect, see |
| // MaybeStartLoader. |
| // TODO(michaeln): This is still WIP and is based on URLRequest::Redirect, |
| // there likely remains more to be done. |
| // a. For subframe navigations, the Origin header may need to be modified |
| // differently? |
| |
| bool should_clear_upload = false; |
| net::RedirectUtil::UpdateHttpRequest( |
| resource_request_->url, resource_request_->method, redirect_info_, |
| removed_headers, modified_headers, &resource_request_->headers, |
| &should_clear_upload); |
| if (should_clear_upload) { |
| // The request body is no longer applicable. |
| resource_request_->request_body.reset(); |
| } |
| |
| resource_request_->url = redirect_info_.new_url; |
| resource_request_->method = redirect_info_.new_method; |
| resource_request_->site_for_cookies = redirect_info_.new_site_for_cookies; |
| |
| // See if navigation network isolation key needs to be updated. |
| resource_request_->trusted_params->isolation_info = |
| resource_request_->trusted_params->isolation_info.CreateForRedirect( |
| url::Origin::Create(resource_request_->url)); |
| |
| resource_request_->referrer = GURL(redirect_info_.new_referrer); |
| resource_request_->referrer_policy = redirect_info_.new_referrer_policy; |
| resource_request_->navigation_redirect_chain.push_back( |
| redirect_info_.new_url); |
| |
| // Need to cache modified headers for `url_loader_` since it doesn't use |
| // `resource_request_` during redirect. |
| url_loader_removed_headers_ = removed_headers; |
| url_loader_modified_headers_ = modified_headers; |
| url_loader_modified_cors_exempt_headers_ = modified_cors_exempt_headers; |
| |
| // Don't send Accept: application/signed-exchange for fallback redirects. |
| if (redirect_info_.is_signed_exchange_fallback_redirect) { |
| std::string header_value = |
| FrameAcceptHeaderValue(/*allow_sxg_responses=*/false, browser_context_); |
| url_loader_modified_headers_.SetHeader(net::HttpRequestHeaders::kAccept, |
| header_value); |
| resource_request_->headers.SetHeader(net::HttpRequestHeaders::kAccept, |
| header_value); |
| } |
| |
| Restart(); |
| } |
| |
| bool NavigationURLLoaderImpl::SetNavigationTimeout(base::TimeDelta timeout) { |
| // If the timer has already been started, don't change it. |
| if (timeout_timer_.IsRunning()) { |
| return false; |
| } |
| |
| // Fail the navigation with error code ERR_TIMED_OUT if the timer triggers |
| // before the navigation commits. (This triggers OnComplete() rather than |
| // NotifyRequestFailed() to make sure that any NavigationLoaderInterceptors |
| // can handle the result if needed.) |
| timeout_timer_.Start( |
| FROM_HERE, timeout, |
| base::BindOnce(&NavigationURLLoaderImpl::OnComplete, |
| base::Unretained(this), |
| network::URLLoaderCompletionStatus(net::ERR_TIMED_OUT))); |
| return true; |
| } |
| |
| void NavigationURLLoaderImpl::CancelNavigationTimeout() { |
| timeout_timer_.Stop(); |
| } |
| |
| void NavigationURLLoaderImpl::NotifyResponseStarted( |
| network::mojom::URLResponseHeadPtr response_head, |
| network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints, |
| mojo::ScopedDataPipeConsumerHandle response_body, |
| const GlobalRequestID& global_request_id, |
| bool is_download) { |
| TRACE_EVENT_NESTABLE_ASYNC_END2( |
| "navigation", "Navigation timeToResponseStarted", TRACE_ID_LOCAL(this), |
| "&NavigationURLLoaderImpl", static_cast<void*>(this), "success", true); |
| |
| NavigationURLLoaderDelegate::EarlyHints early_hints; |
| if (early_hints_manager_) { |
| early_hints.was_resource_hints_received = |
| early_hints_manager_->WasResourceHintsReceived(); |
| |
| // Make Early Hints manager outlive this loader only when the response |
| // headers are available. Dropping the manager cancels inflight preloads. |
| if (response_head && response_head->headers) { |
| early_hints.manager = std::move(early_hints_manager_); |
| } |
| } |
| |
| // TODO(scottmg): This needs to do more of what |
| // NavigationResourceHandler::OnResponseStarted() does. |
| delegate_->OnResponseStarted( |
| std::move(url_loader_client_endpoints), std::move(response_head), |
| std::move(response_body), global_request_id, is_download, |
| resource_request_->trusted_params->isolation_info |
| .network_anonymization_key(), |
| std::move(subresource_loader_params_), std::move(early_hints)); |
| } |
| |
| void NavigationURLLoaderImpl::NotifyRequestRedirected( |
| net::RedirectInfo redirect_info, |
| network::mojom::URLResponseHeadPtr response_head) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| delegate_->OnRequestRedirected( |
| redirect_info, |
| resource_request_->trusted_params->isolation_info |
| .network_anonymization_key(), |
| std::move(response_head)); |
| } |
| |
| void NavigationURLLoaderImpl::NotifyRequestFailed( |
| const network::URLLoaderCompletionStatus& status) { |
| TRACE_EVENT_NESTABLE_ASYNC_END2( |
| "navigation", "Navigation timeToResponseStarted", TRACE_ID_LOCAL(this), |
| "&NavigationURLLoaderImpl", static_cast<void*>(this), "success", false); |
| delegate_->OnRequestFailed(status); |
| } |
| |
| // static |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| NavigationURLLoaderImpl::CreateURLLoaderFactoryWithHeaderClient( |
| mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient> |
| header_client, |
| network::URLLoaderFactoryBuilder factory_builder, |
| StoragePartitionImpl* partition, |
| std::optional<net::CookieSettingOverrides> devtools_cookie_overrides, |
| std::optional<net::CookieSettingOverrides> cookie_overrides) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (url_loader_factory::GetTestingInterceptor()) { |
| url_loader_factory::GetTestingInterceptor().Run( |
| network::mojom::kBrowserProcessId, factory_builder); |
| } |
| |
| network::mojom::URLLoaderFactoryParamsPtr params = |
| network::mojom::URLLoaderFactoryParams::New(); |
| params->header_client = std::move(header_client); |
| params->process_id = network::mojom::kBrowserProcessId; |
| params->is_trusted = true; |
| params->is_orb_enabled = false; |
| params->disable_web_security = |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisableWebSecurity); |
| if (devtools_cookie_overrides.has_value()) { |
| params->devtools_cookie_setting_overrides = |
| std::move(devtools_cookie_overrides.value()); |
| } |
| if (cookie_overrides.has_value()) { |
| params->cookie_setting_overrides = std::move(cookie_overrides.value()); |
| } |
| return std::move(factory_builder) |
| .Finish<mojo::PendingRemote<network::mojom::URLLoaderFactory>>( |
| partition->GetNetworkContext(), std::move(params)); |
| } |
| |
| void NavigationURLLoaderImpl::RecordReceivedResponseUkmForOutermostMainFrame() { |
| FrameTreeNode* frame_tree_node = |
| FrameTreeNode::GloballyFindByID(frame_tree_node_id_); |
| DCHECK(frame_tree_node); |
| |
| auto* ukm_recorder = ukm::UkmRecorder::Get(); |
| ukm::builders::Navigation_ReceivedResponse builder(ukm_source_id_); |
| base::TimeDelta latency = base::TimeTicks::Now() - loader_creation_time_; |
| builder.SetHasAcceptCHFrame(received_accept_ch_frame_) |
| .SetNavigationFirstResponseLatency(latency.InMilliseconds()); |
| builder.Record(ukm_recorder->Get()); |
| |
| // Reset whether the ACCEPT_CH frame was received for the navigation. |
| received_accept_ch_frame_ = false; |
| } |
| |
| void NavigationURLLoaderImpl::MaybeRecordServiceWorkerMainResourceInfo( |
| const network::mojom::URLResponseHeadPtr& head) { |
| CHECK(head); |
| if (!head->initial_service_worker_status.has_value() && |
| !head->service_worker_router_info) { |
| return; |
| } |
| |
| ukm::builders::ServiceWorker_MainResourceLoadCompleted builder( |
| ukm_source_id_); |
| |
| CHECK(head->initial_service_worker_status.has_value()); |
| builder.SetInitialWorkerStatus( |
| static_cast<int64_t>(head->initial_service_worker_status.value())); |
| |
| if (head->service_worker_router_info) { |
| network::mojom::ServiceWorkerRouterInfo* router_info = |
| head->service_worker_router_info.get(); |
| if (router_info->evaluation_worker_status.has_value()) { |
| builder.SetWorkerStatusOnEvaluation( |
| static_cast<int64_t>(router_info->evaluation_worker_status.value())); |
| } |
| // Check if `matched_source_type` and `actual_source_type` exists. If |
| // `matched_source_type` exists, `actual_source_type` should also exist. |
| // Likewise, if `matched_source_type` does not exist, `actual_source_type` |
| // should also not exist. |
| CHECK_EQ(router_info->matched_source_type.has_value(), |
| router_info->actual_source_type.has_value()); |
| if (router_info->matched_source_type) { |
| builder.SetMatchedFirstRouterSourceType( |
| static_cast<int64_t>(*router_info->matched_source_type)); |
| if (router_info->matched_source_type == |
| network::mojom::ServiceWorkerRouterSourceType::kCache) { |
| builder.SetCacheLookupTime( |
| router_info->cache_lookup_time.InMilliseconds()); |
| } |
| } |
| if (router_info->actual_source_type) { |
| builder.SetActualRouterSourceType( |
| static_cast<int64_t>(*router_info->actual_source_type)); |
| } |
| builder |
| .SetRouterRuleCount(ukm::GetExponentialBucketMinForCounts1000( |
| router_info->route_rule_num)) |
| .SetRouterEvaluationTime( |
| router_info->router_evaluation_time.InMicroseconds()); |
| } |
| builder.Record(ukm::UkmRecorder::Get()); |
| } |
| |
| } // namespace content |