| // Copyright 2023 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/keep_alive_url_loader.h" |
| |
| #include <vector> |
| |
| #include "base/check_is_test.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "content/browser/renderer_host/policy_container_host.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/url_loader_throttles.h" |
| #include "content/public/common/url_utils.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_request_headers.h" |
| #include "services/network/public/cpp/content_security_policy/content_security_policy.h" |
| #include "services/network/public/cpp/content_security_policy/csp_context.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/content_security_policy.mojom.h" |
| #include "services/network/public/mojom/early_hints.mojom.h" |
| #include "services/network/public/mojom/fetch_api.mojom-shared.h" |
| #include "services/network/public/mojom/url_request.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| namespace { |
| |
| // Internally enforces a limit to allow a loader outlive its renderer after |
| // receiving disconnection notification from the renderer. |
| // |
| // Defaults to 30s, the same as pre-migration's timeout. |
| constexpr base::TimeDelta kDefaultDisconnectedKeepAliveURLLoaderTimeout = |
| base::Seconds(30); |
| |
| base::TimeDelta GetDisconnectedKeepAliveURLLoaderTimeout() { |
| return base::Seconds(GetFieldTrialParamByFeatureAsInt( |
| blink::features::kKeepAliveInBrowserMigration, |
| "disconnected_loader_timeout_seconds", |
| base::checked_cast<int32_t>( |
| kDefaultDisconnectedKeepAliveURLLoaderTimeout.InSeconds()))); |
| } |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // |
| // Must remain in sync with FetchKeepAliveBrowserMetricType in |
| // tools/metrics/histograms/enums.xml. |
| enum class FetchKeepAliveBrowserMetricType { |
| kLoadingSuceeded = 0, |
| kLoadingFailed = 1, |
| kForwardingCompleted = 2, |
| kCancelledAfterTimeLimit = 3, |
| kAbortedByInitiator = 4, |
| kMaxValue = kAbortedByInitiator, |
| }; |
| |
| void LogFetchKeepAliveMetric(const FetchKeepAliveBrowserMetricType& type) { |
| base::UmaHistogramEnumeration("FetchKeepAlive.Browser.Metrics", type); |
| } |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // |
| // Must remain in sync with FetchLaterBrowserMetricType in |
| // tools/metrics/histograms/enums.xml. |
| enum class FetchLaterBrowserMetricType { |
| kAbortedByInitiator = 0, |
| kStartedAfterInitiatorDisconnected = 1, |
| kStartedByInitiator = 2, |
| kCancelledAfterTimeLimit = 3, |
| kMaxValue = kCancelledAfterTimeLimit, |
| }; |
| |
| void LogFetchLaterMetric(const FetchLaterBrowserMetricType& type) { |
| base::UmaHistogramEnumeration("FetchLater.Browser.Metrics", type); |
| } |
| |
| // A convenient holder to aggregate modified header fields for redirect. |
| struct ModifiedHeaders { |
| ModifiedHeaders() = default; |
| ~ModifiedHeaders() = default; |
| // Not copyable. |
| ModifiedHeaders(const ModifiedHeaders&) = delete; |
| ModifiedHeaders& operator=(const ModifiedHeaders&) = delete; |
| |
| void MergeFrom(const ModifiedHeaders& other) { |
| for (auto& other_removed_header : other.removed_headers) { |
| if (!base::Contains(removed_headers, other_removed_header)) { |
| removed_headers.emplace_back(std::move(other_removed_header)); |
| } |
| } |
| modified_headers.MergeFrom(other.modified_headers); |
| modified_cors_exempt_headers.MergeFrom(other.modified_cors_exempt_headers); |
| } |
| |
| std::vector<std::string> removed_headers; |
| net::HttpRequestHeaders modified_headers; |
| net::HttpRequestHeaders modified_cors_exempt_headers; |
| }; |
| |
| // A ContentSecurityPolicy context for KeepAliveURLLoader. |
| class KeepAliveURLLoaderCSPContext final : public network::CSPContext { |
| public: |
| // network::CSPContext override: |
| void ReportContentSecurityPolicyViolation( |
| network::mojom::CSPViolationPtr violation_params) final { |
| // TODO(crbug.com/1356128): Support reporting violation w/o renderer. |
| } |
| void SanitizeDataForUseInCspViolation( |
| network::mojom::CSPDirectiveName directive, |
| GURL* blocked_url, |
| network::mojom::SourceLocation* source_location) const final { |
| // TODO(crbug.com/1356128): Support reporting violation w/o renderer. |
| } |
| }; |
| |
| // Checks if `url` is allowed by the set of Content-Security-Policy `policies`. |
| // Violation will not be reported back to renderer, as this function must be |
| // called after renderer is gone. |
| // TODO(crbug.com/1431165): Isolated world's CSP is not handled. |
| bool IsRedirectAllowedByCSP( |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& policies, |
| const GURL& url, |
| const GURL& url_before_redirects, |
| bool has_followed_redirect) { |
| // Sets the CSP Directive for fetch() requests. See |
| // https://w3c.github.io/webappsec-csp/#directive-connect-src |
| // https://fetch.spec.whatwg.org/#destination-table |
| auto directive = network::mojom::CSPDirectiveName::ConnectSrc; |
| // Sets empty as source location is only used when reporting back to renderer. |
| auto empty_source_location = network::mojom::SourceLocation::New(); |
| auto disposition = network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP; |
| |
| // When reaching here, renderer should have be gone, or at least |
| // `KeepAliveURLLoader::forwarding_client_` is disconnected. |
| return KeepAliveURLLoaderCSPContext().IsAllowedByCsp( |
| policies, directive, url, url_before_redirects, has_followed_redirect, |
| /*is_response_check=*/false, empty_source_location, disposition, |
| /*is_form_submission=*/false); |
| } |
| |
| } // namespace |
| |
| // A custom `blink::URLLoaderThrottle` delegate that only handles relevant |
| // actions. |
| // |
| // Note that a delegate may be called from a throttle asynchronously in a |
| // different thread, e.g. `safe_browsing::BrowserURLLoaderThrottle` runs in IO |
| // thread http://crbug.com/1057253. |
| // |
| // Throttles calling these methods must not be destroyed synchronously. |
| class KeepAliveURLLoader::ThrottleDelegate final |
| : public blink::URLLoaderThrottle::Delegate { |
| public: |
| explicit ThrottleDelegate(base::WeakPtr<KeepAliveURLLoader> loader) |
| : loader_(std::move(loader)), |
| loader_weak_ptr_factory_( |
| std::make_unique<base::WeakPtrFactory<KeepAliveURLLoader>>( |
| loader_.get())) {} |
| // Not copyable. |
| ThrottleDelegate(const ThrottleDelegate&) = delete; |
| ThrottleDelegate& operator=(const ThrottleDelegate&) = delete; |
| |
| // blink::URLLoaderThrottle::Delegate overrides: |
| // Asks `loader_` to abort itself asynchronously. |
| void CancelWithError(int error, base::StringPiece custom_reason) override { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&KeepAliveURLLoader::OnComplete, |
| loader_weak_ptr_factory_->GetWeakPtr(), |
| network::URLLoaderCompletionStatus(error))); |
| return; |
| } |
| if (IsLoaderAliveOnUI()) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&KeepAliveURLLoader::OnComplete, loader_->GetWeakPtr(), |
| network::URLLoaderCompletionStatus(error))); |
| } |
| } |
| void Resume() override { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&KeepAliveURLLoader::ResumeReadingBodyFromNet, |
| loader_weak_ptr_factory_->GetWeakPtr())); |
| return; |
| } |
| if (IsLoaderAliveOnUI()) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&KeepAliveURLLoader::ResumeReadingBodyFromNet, |
| loader_->GetWeakPtr())); |
| } |
| } |
| void PauseReadingBodyFromNet() override { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&KeepAliveURLLoader::PauseReadingBodyFromNet, |
| loader_weak_ptr_factory_->GetWeakPtr())); |
| return; |
| } |
| if (IsLoaderAliveOnUI()) { |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(&KeepAliveURLLoader::PauseReadingBodyFromNet, |
| loader_->GetWeakPtr())); |
| } |
| } |
| void RestartWithFlags(int additional_load_flags) override { NOTREACHED(); } |
| void RestartWithURLResetAndFlags(int additional_load_flags) override { |
| NOTREACHED(); |
| } |
| |
| private: |
| // `loader_` is alive and ready to take actions triggered from in-browser |
| // throttle, i.e. `loader_` is disconnected from renderer. Otherwise, returns |
| // false to avoid early termination when a copy of the same throttle will also |
| // be executed in renderer. |
| // Must be called on UI thread. |
| bool IsLoaderAliveOnUI() const { |
| CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| return loader_ && !loader_->IsRendererConnected(); |
| } |
| |
| base::WeakPtr<KeepAliveURLLoader> loader_; |
| // `loader_` lives in UI thread. This factory helps verify if `loader_` is |
| // still available from other threads. |
| std::unique_ptr<base::WeakPtrFactory<KeepAliveURLLoader>> |
| loader_weak_ptr_factory_; |
| }; |
| |
| // Maintains a `blink::URLLoaderThrottle` and its delegate's lifetime. |
| class KeepAliveURLLoader::ThrottleEntry { |
| public: |
| ThrottleEntry(base::WeakPtr<KeepAliveURLLoader> loader, |
| std::unique_ptr<blink::URLLoaderThrottle> loader_throttle) |
| : delegate_(std::make_unique<ThrottleDelegate>(std::move(loader))), |
| throttle_(std::move(loader_throttle)) { |
| CHECK(delegate_); |
| CHECK(throttle_); |
| throttle_->set_delegate(delegate_.get()); |
| } |
| ~ThrottleEntry() { |
| // Both `delegate_` and `throttle_` are about to be destroyed, but |
| // `throttle_` may refer to `delegate_` in its dtor. Hence, clear the |
| // pointer from `throttle_` to avoid any UAF. |
| throttle_->set_delegate(nullptr); |
| } |
| // Not copyable. |
| ThrottleEntry(const ThrottleEntry&) = delete; |
| ThrottleEntry& operator=(const ThrottleEntry&) = delete; |
| |
| blink::URLLoaderThrottle& throttle() { return *throttle_; } |
| |
| private: |
| // `delegate_` must live longer than `throttle_`. |
| std::unique_ptr<ThrottleDelegate> delegate_; |
| std::unique_ptr<blink::URLLoaderThrottle> throttle_; |
| }; |
| |
| // Stores the chain of redriects, response, and completion status, such that |
| // they can be forwarded to renderer after handled in browser first. |
| // See also `ForwardURLLoad()`. |
| struct KeepAliveURLLoader::StoredURLLoad { |
| StoredURLLoad() = default; |
| |
| // Not copyable. |
| StoredURLLoad(const StoredURLLoad&) = delete; |
| StoredURLLoad& operator=(const StoredURLLoad&) = delete; |
| |
| // Stores data for a redirect received from `OnReceiveRedirect()`. |
| struct RedirectData { |
| RedirectData(const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr response_head) |
| : info(redirect_info), head(std::move(response_head)) {} |
| // Not copyable. |
| RedirectData(const RedirectData&) = delete; |
| RedirectData& operator=(const RedirectData&) = delete; |
| |
| // A copy of the RedirectInfo. |
| net::RedirectInfo info; |
| // The original URLResponseHead not yet passed to renderer. |
| network::mojom::URLResponseHeadPtr head; |
| }; |
| |
| // Stores data for a response received from `OnReceiveResponse()`. |
| struct ResponseData { |
| ResponseData(network::mojom::URLResponseHeadPtr response_head, |
| mojo::ScopedDataPipeConsumerHandle body_handle, |
| absl::optional<mojo_base::BigBuffer> cached_metadata) |
| : head(std::move(response_head)), |
| body(std::move(body_handle)), |
| metadata(std::move(cached_metadata)) {} |
| // Not copyable. |
| ResponseData(const ResponseData&) = delete; |
| ResponseData& operator=(const ResponseData&) = delete; |
| |
| // The original URLResponseHead not yet passed to renderer. |
| network::mojom::URLResponseHeadPtr head; |
| // The original body handle not yet passed to renderer. |
| mojo::ScopedDataPipeConsumerHandle body; |
| // The original cached metadata not yet passed to renderer. |
| absl::optional<mojo_base::BigBuffer> metadata; |
| }; |
| |
| // Stores all intermediate redirect data received from `OnReceiveRedirect()`. |
| std::queue<std::unique_ptr<RedirectData>> redirects; |
| // Stores the response data received from `OnReceiveResponse()` for later use |
| // in renderer. |
| std::unique_ptr<ResponseData> response = nullptr; |
| // Stores the completion status received from `OnComplete()` for later use in |
| // renderer. |
| absl::optional<network::URLLoaderCompletionStatus> completion_status = |
| absl::nullopt; |
| // Tells whether any of the above field has been used (forwarded to renderer). |
| bool forwarding = false; |
| }; |
| |
| KeepAliveURLLoader::KeepAliveURLLoader( |
| int32_t request_id, |
| uint32_t options, |
| const network::ResourceRequest& resource_request, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> forwarding_client, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, |
| scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory, |
| scoped_refptr<PolicyContainerHost> policy_container_host, |
| BrowserContext* browser_context, |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles, |
| base::PassKey<KeepAliveURLLoaderService>) |
| : request_id_(request_id), |
| options_(options), |
| resource_request_(resource_request), |
| forwarding_client_(std::move(forwarding_client)), |
| traffic_annotation_(traffic_annotation), |
| network_loader_factory_(std::move(network_loader_factory)), |
| stored_url_load_(std::make_unique<StoredURLLoad>()), |
| policy_container_host_(std::move(policy_container_host)), |
| browser_context_(browser_context), |
| initial_url_(resource_request.url), |
| last_url_(resource_request.url) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(network_loader_factory_); |
| CHECK(policy_container_host_); |
| CHECK(!resource_request.trusted_params); |
| CHECK(browser_context_); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::KeepAliveURLLoader", "request_id", |
| request_id_, "url", last_url_); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("loading", "KeepAliveURLLoader", |
| request_id_, "url", last_url_); |
| if (IsFetchLater()) { |
| base::UmaHistogramBoolean("FetchLater.Browser.Total", true); |
| } |
| base::UmaHistogramBoolean("FetchKeepAlive.Browser.Total", true); |
| |
| // TODO(crbug.com/1356128): Replace custom throttle logic here with blink's. |
| for (auto& content_throttle : throttles) { |
| throttle_entries_.emplace_back(std::make_unique<ThrottleEntry>( |
| GetWeakPtr(), std::move(content_throttle))); |
| } |
| } |
| |
| void KeepAliveURLLoader::Start() { |
| CHECK(!is_started_); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::Start", "request_id", |
| request_id_); |
| is_started_ = true; |
| |
| if (IsFetchLater()) { |
| base::UmaHistogramBoolean("FetchLater.Browser.Total.Started", true); |
| } |
| base::UmaHistogramBoolean("FetchKeepAlive.Browser.Total.Started", true); |
| |
| // Asks the network service to create a URL loader with passed in params. |
| network_loader_factory_->CreateLoaderAndStart( |
| loader_.BindNewPipeAndPassReceiver(), request_id_, options_, |
| resource_request_, loader_receiver_.BindNewPipeAndPassRemote(), |
| traffic_annotation_); |
| loader_receiver_.set_disconnect_handler(base::BindOnce( |
| &KeepAliveURLLoader::OnNetworkConnectionError, base::Unretained(this))); |
| // For FetchLater requests, `forwarding_client_` is not bound to renderer. |
| if (forwarding_client_) { |
| forwarding_client_.set_disconnect_handler( |
| base::BindOnce(&KeepAliveURLLoader::OnForwardingClientDisconnected, |
| base::Unretained(this))); |
| } |
| |
| // These throttles are also run by `blink::ThrottlingURLLoader`. However, they |
| // have to be re-run here in case of handling in-browser redirects. |
| // There is already a similar use case that also runs throttles in browser in |
| // `SearchPrefetchRequest::StartPrefetchRequest()`. The review discussion in |
| // https://crrev.com/c/2552723/3 suggests that running them again in browser |
| // is fine. |
| for (auto& throttle_entry : throttle_entries_) { |
| TRACE_EVENT("loading", |
| "KeepAliveURLLoader::KeepAliveURLLoader.WillStartRequest"); |
| bool throttle_deferred = false; |
| auto weak_ptr = GetWeakPtr(); |
| // Marks delegate to ignore abort requests if this is connected to renderer. |
| throttle_entry->throttle().WillStartRequest(&resource_request_, |
| &throttle_deferred); |
| if (!weak_ptr) { |
| // `this` is already destroyed by throttle. |
| return; |
| } |
| if (!IsRendererConnected() && throttle_deferred) { |
| // Only processes a throttle result if this loader is already disconnected |
| // from renderer. We treat deferring as canceling the request. |
| // See also `ThrottleDelegate` which may cancel request asynchronously. |
| OnComplete(network::URLLoaderCompletionStatus(net::ERR_ABORTED)); |
| return; |
| } |
| } |
| } |
| |
| KeepAliveURLLoader::~KeepAliveURLLoader() { |
| TRACE_EVENT("loading", "KeepAliveURLLoader::~KeepAliveURLLoader", |
| "request_id", request_id_); |
| TRACE_EVENT_NESTABLE_ASYNC_END0("loading", "KeepAliveURLLoader", request_id_); |
| |
| disconnected_loader_timer_.Stop(); |
| } |
| |
| void KeepAliveURLLoader::set_on_delete_callback( |
| OnDeleteCallback on_delete_callback) { |
| on_delete_callback_ = std::move(on_delete_callback); |
| } |
| |
| base::WeakPtr<KeepAliveURLLoader> KeepAliveURLLoader::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| bool KeepAliveURLLoader::IsStarted() const { |
| return is_started_; |
| } |
| |
| void KeepAliveURLLoader::FollowRedirect( |
| const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const net::HttpRequestHeaders& modified_cors_exempt_headers, |
| const absl::optional<GURL>& new_url) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::FollowRedirect", "request_id", |
| request_id_, "url", new_url); |
| |
| if (new_url != absl::nullopt) { |
| mojo::ReportBadMessage( |
| "Unexpected `new_url` in KeepAliveURLLoader::FollowRedirect(): " |
| "must be null"); |
| return; |
| } |
| |
| if (IsRendererConnected()) { |
| // Continue forwarding the stored data to renderer. |
| // Note: we may or may not have response at this point. |
| ForwardURLLoad(); |
| // DO NOT touch any members after this line. `this` may be already deleted |
| // if `OnComplete()` has been triggered. |
| return; |
| } |
| // No need to forward anymore as the target renderer is gone. |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is deleted. |
| } |
| |
| void KeepAliveURLLoader::SetPriority(net::RequestPriority priority, |
| int intra_priority_value) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::SetPriority", "request_id", |
| request_id_); |
| |
| // Forwards the action to `loader_` in the network service. |
| loader_->SetPriority(priority, intra_priority_value); |
| } |
| |
| void KeepAliveURLLoader::PauseReadingBodyFromNet() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::PauseReadingBodyFromNet", |
| "request_id", request_id_); |
| if (HasReceivedResponse()) { |
| // This may come from a renderer that tries to process a redirect which has |
| // been previously handled in this loader. |
| return; |
| } |
| |
| if (paused_reading_body_from_net_count_ == 0) { |
| // Only sends the action to `loader_` in the network service once before |
| // resuming. |
| loader_->PauseReadingBodyFromNet(); |
| } |
| paused_reading_body_from_net_count_++; |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->PauseReadingBodyFromNetProcessed(this); |
| } |
| } |
| |
| // TODO(crbug.com/1356128): Add test coverage. |
| void KeepAliveURLLoader::ResumeReadingBodyFromNet() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::ResumeReadingBodyFromNet", |
| "request_id", request_id_); |
| if (HasReceivedResponse()) { |
| // This may come from a renderer that tries to process a redirect which has |
| // been previously handled in this loader. |
| return; |
| } |
| |
| if (paused_reading_body_from_net_count_ == 1) { |
| // Sends the action to `loader_` in the network service. |
| loader_->ResumeReadingBodyFromNet(); |
| } |
| paused_reading_body_from_net_count_--; |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->ResumeReadingBodyFromNetProcessed(this); |
| } |
| } |
| |
| void KeepAliveURLLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnReceiveEarlyHints", |
| "request_id", request_id_); |
| |
| if (IsRendererConnected()) { |
| // The renderer is alive, forwards the action. |
| forwarding_client_->OnReceiveEarlyHints(std::move(early_hints)); |
| return; |
| } |
| |
| // TODO(crbug.com/1356128): Handle in browser process. |
| } |
| |
| void KeepAliveURLLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnReceiveRedirect", "request_id", |
| request_id_); |
| base::UmaHistogramBoolean("FetchKeepAlive.Browser.Total.Redirected", true); |
| |
| // Stores the redirect data for later use by renderer. |
| stored_url_load_->redirects.emplace( |
| std::make_unique<StoredURLLoad::RedirectData>(redirect_info, |
| std::move(head))); |
| |
| // Handles all redirects in browser first. |
| // See also the call sequence from renderer: |
| // https://docs.google.com/document/d/1ZzxMMBvpqn8VZBZKnb7Go8TWjnrGcXuLS_USwVVRUvY/edit?pli=1#heading=h.d006i46pmq9 |
| |
| // Runs throttles from content embedder. |
| ModifiedHeaders modified; |
| for (auto& throttle_entry : throttle_entries_) { |
| TRACE_EVENT("loading", |
| "KeepAliveURLLoader::OnReceiveRedirect.WillRedirectRequest"); |
| bool throttle_deferred = false; |
| ModifiedHeaders throttle_modified; |
| net::RedirectInfo redirect_info_copy = redirect_info; |
| auto weak_ptr = GetWeakPtr(); |
| throttle_entry->throttle().WillRedirectRequest( |
| &redirect_info_copy, *(stored_url_load_->redirects.back()->head), |
| &throttle_deferred, &throttle_modified.removed_headers, |
| &throttle_modified.modified_headers, |
| &throttle_modified.modified_cors_exempt_headers); |
| if (!weak_ptr) { |
| // `this` is already destroyed by throttle. |
| return; |
| } |
| CHECK_EQ(redirect_info_copy.new_url, redirect_info.new_url) |
| << "KeepAliveURLLoader doesn't support throttles changing the URL."; |
| |
| if (throttle_deferred) { |
| // We treat deferring as canceling the request. |
| // See also `ThrottleDelegate` which may cancel request asynchronously. |
| OnComplete(network::URLLoaderCompletionStatus(net::ERR_ABORTED)); |
| return; |
| } |
| modified.MergeFrom(throttle_modified); |
| } |
| |
| if (net::Error err = WillFollowRedirect(redirect_info); err != net::OK) { |
| OnComplete(network::URLLoaderCompletionStatus(err)); |
| return; |
| } |
| |
| // TODO(crbug.com/1356128): Replicate critical logic from the followings: |
| // `ResourceRequestSender::OnReceivedRedirect()`. |
| // `URLLoader::Context::OnReceivedRedirect(). |
| // TODO(crbug.com/1356128): Figure out how to deal with lost ResourceFetcher's |
| // counter & dev console logging (renderer is dead). |
| |
| resource_request_.url = redirect_info.new_url; |
| resource_request_.site_for_cookies = redirect_info.new_site_for_cookies; |
| resource_request_.referrer = GURL(redirect_info.new_referrer); |
| resource_request_.referrer_policy = redirect_info.new_referrer_policy; |
| // Ask the network service to follow the redirect. |
| last_url_ = GURL(redirect_info.new_url); |
| // TODO(crbug.com/1393520): Remove Authorization header upon cross-origin |
| // redirect. |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnReceiveRedirectProcessed(this); |
| } |
| |
| // Directly forwards the action to `loader_` in the network service. |
| // |
| // Follows redirect only after all current throttle UI tasks are executed. |
| // Note: there may be throttles running in IO thread, which may send signals |
| // in between `FollowRedirect()` and the next `OnReceiveRedirect()` or |
| // `OnReceiveResponse()`. |
| loader_->FollowRedirect(modified.removed_headers, modified.modified_headers, |
| modified.modified_cors_exempt_headers, |
| /*new_url=*/absl::nullopt); |
| } |
| |
| void KeepAliveURLLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response, |
| mojo::ScopedDataPipeConsumerHandle body, |
| absl::optional<mojo_base::BigBuffer> cached_metadata) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnReceiveResponse", "request_id", |
| request_id_, "url", last_url_); |
| base::UmaHistogramBoolean("FetchKeepAlive.Browser.Total.ReceivedResponse", |
| true); |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnReceiveResponse(this); |
| } |
| |
| // In case the renderer is alive, the stored response data will be forwarded |
| // at the end of `ForwardURLLoad()`. |
| stored_url_load_->response = std::make_unique<StoredURLLoad::ResponseData>( |
| std::move(response), std::move(body), std::move(cached_metadata)); |
| |
| // TODO(crbug.com/1422645): Ensure that attributionsrc response handling is |
| // migrated to browser process here so that it works even when renderer is |
| // disconnected. |
| // For now, it happens in the renderer after response is forwarded. |
| |
| if (IsRendererConnected()) { |
| // Starts to forward the stored redirects/response to renderer. |
| // Note that `OnComplete()` may be triggered in between the forwarding. |
| ForwardURLLoad(); |
| // DO NOT touch any members after this line. `this` may be already deleted |
| // if `OnComplete()` has been triggered. |
| return; |
| } |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnReceiveResponseProcessed(this); |
| } |
| |
| // No need to wait for `OnComplete()`. |
| // This loader should be deleted immediately to avoid hanged requests taking |
| // up resources. |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| } |
| |
| void KeepAliveURLLoader::OnUploadProgress(int64_t current_position, |
| int64_t total_size, |
| base::OnceCallback<void()> callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnUploadProgress", "request_id", |
| request_id_); |
| |
| if (IsRendererConnected()) { |
| // The renderer is alive, forwards the action. |
| forwarding_client_->OnUploadProgress(current_position, total_size, |
| std::move(callback)); |
| return; |
| } |
| |
| // TODO(crbug.com/1356128): Handle in the browser process. |
| } |
| |
| void KeepAliveURLLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnTransferSizeUpdated", |
| "request_id", request_id_); |
| |
| if (IsRendererConnected()) { |
| // The renderer is alive, forwards the action. |
| forwarding_client_->OnTransferSizeUpdated(transfer_size_diff); |
| return; |
| } |
| |
| // TODO(crbug.com/1356128): Handle in the browser process. |
| } |
| |
| void KeepAliveURLLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& completion_status) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnComplete", "request_id", |
| request_id_); |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnComplete(this, completion_status); |
| } |
| |
| LogFetchKeepAliveMetric( |
| completion_status.error_code == net::OK |
| ? FetchKeepAliveBrowserMetricType::kLoadingSuceeded |
| : FetchKeepAliveBrowserMetricType::kLoadingFailed); |
| |
| // In case the renderer is alive, the stored status will be forwarded |
| // at the end of `ForwardURLLoad()`. |
| stored_url_load_->completion_status = completion_status; |
| |
| if (IsRendererConnected()) { |
| if (HasReceivedResponse()) { |
| // Do nothing. `completion_status` will be forwarded at the end of |
| // `ForwardURLLoad()`. |
| return; |
| } |
| |
| // Either (1) an error happens in between redirect handling in browser, or |
| // (2) the redirects and response have all been forwarded. |
| // Starts forwarding stored redirects and the completion status to renderer. |
| ForwardURLLoad(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| return; |
| } |
| |
| // TODO(crbug.com/1356128): Handle in the browser process. |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnCompleteProcessed(this, completion_status); |
| } |
| |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| } |
| |
| bool KeepAliveURLLoader::HasReceivedResponse() const { |
| return stored_url_load_ && stored_url_load_->response != nullptr; |
| } |
| |
| void KeepAliveURLLoader::ForwardURLLoad() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(IsRendererConnected()); |
| CHECK(stored_url_load_); |
| |
| // Forwards the redirects/response/completion in the exact sequence. |
| stored_url_load_->forwarding = true; |
| |
| if (!stored_url_load_->redirects.empty()) { |
| // All redirects have been handled in the browser. However, redirects must |
| // also be processed by the renderer so that it knows what URL the |
| // response come from when parsing the response. |
| // |
| // Note: The renderer might get shut down before |
| // `forwarding_client_->OnReceiveRedirect()` finish all redirect handling. |
| // In such case, the handling will be taken over by browser from |
| // `OnRendererConnectionError()`. |
| forwarding_client_->OnReceiveRedirect( |
| stored_url_load_->redirects.front()->info, |
| std::move(stored_url_load_->redirects.front()->head)); |
| stored_url_load_->redirects.pop(); |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnReceiveRedirectForwarded(this); |
| } |
| // The rest of `stored_url_load_->redirects` will be forwarded in the next |
| // visit to this method when `FollowRedirect()` is called by the renderer. |
| return; |
| } |
| |
| if (stored_url_load_->response) { |
| // Note: The receiver may fail to finish reading the entire |
| // `stored_url_load_->response`response`, so response caching is not |
| // guaranteed. |
| // Note: The renderer might get shut down before |
| // `forwarding_client_->OnReceiveResponse()` finish response handling. |
| // In such case, the attributionsrc handling cannot be dropped and should be |
| // taken over by browser in `OnRendererConnectionError(). |
| forwarding_client_->OnReceiveResponse( |
| std::move(stored_url_load_->response->head), |
| std::move(stored_url_load_->response->body), |
| std::move(stored_url_load_->response->metadata)); |
| stored_url_load_->response = nullptr; |
| |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnReceiveResponseForwarded(this); |
| } |
| } |
| |
| if (stored_url_load_->completion_status.has_value()) { |
| forwarding_client_->OnComplete(*(stored_url_load_->completion_status)); |
| if (observer_for_testing_) { |
| CHECK_IS_TEST(); |
| observer_for_testing_->OnCompleteForwarded( |
| this, *(stored_url_load_->completion_status)); |
| } |
| stored_url_load_ = nullptr; |
| LogFetchKeepAliveMetric( |
| FetchKeepAliveBrowserMetricType::kForwardingCompleted); |
| |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| } |
| } |
| |
| bool KeepAliveURLLoader::IsForwardURLLoadStarted() const { |
| return stored_url_load_ && stored_url_load_->forwarding; |
| } |
| |
| bool KeepAliveURLLoader::IsRendererConnected() const { |
| return !!forwarding_client_; |
| } |
| |
| net::Error KeepAliveURLLoader::WillFollowRedirect( |
| const net::RedirectInfo& redirect_info) const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // TODO(crbug.com/1356128): Add logic to handle redirecting to extensions from |
| // `ChromeContentRendererClient::IsSafeRedirectTarget()`. |
| if (!IsSafeRedirectTarget(last_url_, redirect_info.new_url)) { |
| return net::ERR_UNSAFE_REDIRECT; |
| } |
| |
| if (resource_request_.redirect_mode == network::mojom::RedirectMode::kError) { |
| return net::ERR_FAILED; |
| } |
| |
| if (resource_request_.redirect_mode != |
| network::mojom::RedirectMode::kManual) { |
| // Checks if redirecting to `url` is allowed by ContentSecurityPolicy from |
| // the request initiator document. |
| if (!IsRedirectAllowedByCSP( |
| policy_container_host_->policies().content_security_policies, |
| redirect_info.new_url, initial_url_, last_url_ != initial_url_)) { |
| return net::ERR_BLOCKED_BY_CSP; |
| } |
| |
| // TODO(crbug.com/1356128): Refactor logic from |
| // `blink::MixedContentChecker::ShouldBlockFetch()` to support checking |
| // without a frame. |
| } |
| |
| return net::OK; |
| } |
| |
| // Browser <- Network connection. |
| void KeepAliveURLLoader::OnNetworkConnectionError() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnNetworkConnectionError", |
| "request_id", request_id_); |
| |
| // The network loader either has an error or gets disconnected after response |
| // handling is completed. |
| if (IsRendererConnected()) { |
| if (!IsForwardURLLoadStarted()) { |
| // The network service disconnects before this loader forwards anything to |
| // renderer. |
| ForwardURLLoad(); |
| // DO NOT touch any members after this line. `this` may be deleted. |
| return; |
| } |
| // Otherwise, let `ForwardURLLoad()` continue forwarding the rest. |
| return; |
| } |
| |
| // We should let the renderer know it's closed by deleting `this`. |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| } |
| |
| // Browser -> Renderer connection |
| void KeepAliveURLLoader::OnForwardingClientDisconnected() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnForwardingClientDisconnected", |
| "request_id", request_id_); |
| |
| // Dropping the client as renderer is gone. |
| forwarding_client_.reset(); |
| |
| if (!IsForwardURLLoadStarted() && !HasReceivedResponse()) { |
| // The renderer disconnects before this loader forwards anything to it. |
| // But the in-browser request processing may not complete yet. |
| |
| // TODO(crbug.com/1422645): Ensure that attributionsrc response handling is |
| // taken over by browser. |
| return; |
| } |
| |
| // Renderer disconnects in-between forwarding, no need to call |
| // `ForwardURLLoad()` anymore. |
| DeleteSelf(); |
| // DO NOT touch any members after this line. `this` is already deleted. |
| } |
| |
| // Browser <- Renderer connection. |
| void KeepAliveURLLoader::OnURLLoaderDisconnected() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| TRACE_EVENT("loading", "KeepAliveURLLoader::OnURLLoaderDisconnected", |
| "request_id", request_id_); |
| CHECK(!disconnected_loader_timer_.IsRunning()); |
| |
| if (!IsStarted()) { |
| // May be the last chance to start a deferred loader. |
| LogFetchLaterMetric( |
| FetchLaterBrowserMetricType::kStartedAfterInitiatorDisconnected); |
| Start(); |
| } |
| // For other types of keepalive requests, this loader does not care about |
| // whether messages can be received from renderer or not. |
| |
| // Prevents this loader from staying alive indefinitely. |
| disconnected_loader_timer_.Start( |
| FROM_HERE, GetDisconnectedKeepAliveURLLoaderTimeout(), |
| base::BindOnce(&KeepAliveURLLoader::OnDisconnectedLoaderTimerFired, |
| // `this` owns `disconnected_loader_timer_`. |
| base::Unretained(this))); |
| } |
| |
| void KeepAliveURLLoader::OnDisconnectedLoaderTimerFired() { |
| if (IsFetchLater()) { |
| LogFetchLaterMetric(FetchLaterBrowserMetricType::kCancelledAfterTimeLimit); |
| } |
| LogFetchKeepAliveMetric( |
| FetchKeepAliveBrowserMetricType::kCancelledAfterTimeLimit); |
| DeleteSelf(); |
| } |
| |
| bool KeepAliveURLLoader::IsFetchLater() const { |
| return base::FeatureList::IsEnabled(blink::features::kFetchLaterAPI) && |
| resource_request_.is_fetch_later_api; |
| } |
| |
| void KeepAliveURLLoader::SendNow() { |
| if (!IsFetchLater()) { |
| mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::SendNow()"); |
| return; |
| } |
| LogFetchLaterMetric(FetchLaterBrowserMetricType::kStartedByInitiator); |
| if (!IsStarted()) { |
| Start(); |
| } |
| } |
| |
| void KeepAliveURLLoader::Cancel() { |
| if (!IsFetchLater()) { |
| mojo::ReportBadMessage("Unexpected call to KeepAliveURLLoader::Cancel()"); |
| return; |
| } |
| LogFetchLaterMetric(FetchLaterBrowserMetricType::kAbortedByInitiator); |
| DeleteSelf(); |
| } |
| |
| void KeepAliveURLLoader::DeleteSelf() { |
| CHECK(on_delete_callback_); |
| base::UmaHistogramBoolean("FetchKeepAlive.Browser.Total.Finished", true); |
| std::move(on_delete_callback_).Run(); |
| } |
| |
| void KeepAliveURLLoader::SetObserverForTesting( |
| scoped_refptr<TestObserver> observer) { |
| observer_for_testing_ = observer; |
| } |
| |
| } // namespace content |