| // Copyright 2018 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/worker_host/worker_script_loader.h" |
| |
| #include "base/functional/bind.h" |
| #include "content/browser/loader/navigation_loader_interceptor.h" |
| #include "content/browser/loader/response_head_update_params.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/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/common/url_utils.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/load_timing_info.h" |
| #include "net/base/net_errors.h" |
| #include "net/url_request/redirect_util.h" |
| #include "services/network/public/cpp/record_ontransfersizeupdate_utils.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/early_hints.mojom.h" |
| |
| namespace content { |
| |
| WorkerScriptLoader::WorkerScriptLoader( |
| ChildProcessId process_id, |
| const DedicatedOrSharedWorkerToken& worker_token, |
| int32_t request_id, |
| uint32_t options, |
| const network::ResourceRequest& resource_request, |
| const net::IsolationInfo& isolation_info, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client, |
| base::WeakPtr<ServiceWorkerMainResourceHandle> service_worker_handle, |
| const BrowserContextGetter& browser_context_getter, |
| scoped_refptr<network::SharedURLLoaderFactory> default_loader_factory, |
| const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) |
| : request_id_(request_id), |
| options_(options), |
| resource_request_(resource_request), |
| client_(std::move(client)), |
| service_worker_handle_(std::move(service_worker_handle)), |
| browser_context_getter_(browser_context_getter), |
| default_loader_factory_(std::move(default_loader_factory)), |
| traffic_annotation_(traffic_annotation) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| |
| if (!service_worker_handle_) { |
| // The DedicatedWorkerHost or SharedWorkerHost is already destroyed. |
| Abort(); |
| return; |
| } |
| interceptor_ = ServiceWorkerMainResourceLoaderInterceptor::CreateForWorker( |
| resource_request_, isolation_info, process_id, worker_token, |
| service_worker_handle_); |
| |
| Start(); |
| } |
| |
| WorkerScriptLoader::~WorkerScriptLoader() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| } |
| |
| base::WeakPtr<WorkerScriptLoader> WorkerScriptLoader::GetWeakPtr() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void WorkerScriptLoader::Abort() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| complete_status_ = network::URLLoaderCompletionStatus(net::ERR_ABORTED); |
| CommitCompleted(); |
| } |
| |
| void WorkerScriptLoader::Start() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| CHECK_EQ(state_, State::kInitial); |
| |
| // The DedicatedWorkerHost or SharedWorkerHost is already destroyed. |
| if (!service_worker_handle_) { |
| Abort(); |
| return; |
| } |
| |
| BrowserContext* browser_context = browser_context_getter_.Run(); |
| if (!browser_context) { |
| Abort(); |
| return; |
| } |
| |
| if (interceptor_) { |
| interceptor_->MaybeCreateLoader( |
| resource_request_, browser_context, |
| base::BindOnce(&WorkerScriptLoader::MaybeStartLoader, |
| weak_factory_.GetWeakPtr(), interceptor_.get()), |
| base::BindOnce(&WorkerScriptLoader::Fallback, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| LoadFromNetwork(); |
| } |
| |
| network::mojom::URLLoaderFactory* WorkerScriptLoader::Fallback( |
| base::WeakPtr<WorkerScriptLoader> self, |
| ResponseHeadUpdateParams) { |
| if (!self) { |
| return nullptr; |
| } |
| return self->default_loader_factory_.get(); |
| } |
| |
| void WorkerScriptLoader::MaybeStartLoader( |
| ServiceWorkerMainResourceLoaderInterceptor* interceptor, |
| std::optional<NavigationLoaderInterceptor::Result> interceptor_result) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| CHECK_EQ(state_, State::kInitial); |
| CHECK(interceptor, base::NotFatalUntil::M160); |
| |
| if (!service_worker_handle_) { |
| // The DedicatedWorkerHost or SharedWorkerHost is already destroyed. |
| Abort(); |
| return; |
| } |
| |
| // `interceptor_result->subresource_loader_params` isn't set by |
| // ServiceWorkerMainResourceLoaderInterceptor and thus is ignored here. |
| |
| if (interceptor_result && interceptor_result->single_request_factory) { |
| // The interceptor elected to handle the request. Use it. |
| url_loader_factory_ = std::move(interceptor_result->single_request_factory); |
| url_loader_.reset(); |
| url_loader_factory_->CreateLoaderAndStart( |
| url_loader_.BindNewPipeAndPassReceiver(), request_id_, options_, |
| resource_request_, |
| url_loader_client_receiver_.BindNewPipeAndPassRemote(), |
| traffic_annotation_); |
| // We continue in URLLoaderClient calls. |
| return; |
| } |
| |
| // The interceptor didn't elect to handle the request. Fallback to network. |
| LoadFromNetwork(); |
| } |
| |
| void WorkerScriptLoader::LoadFromNetwork() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| CHECK_EQ(state_, State::kInitial); |
| |
| url_loader_client_receiver_.reset(); |
| url_loader_factory_ = default_loader_factory_; |
| url_loader_.reset(); |
| url_loader_factory_->CreateLoaderAndStart( |
| url_loader_.BindNewPipeAndPassReceiver(), request_id_, options_, |
| resource_request_, url_loader_client_receiver_.BindNewPipeAndPassRemote(), |
| traffic_annotation_); |
| // We continue in URLLoaderClient calls. |
| } |
| |
| // URLLoader ------------------------------------------------------------------- |
| // When this class gets a FollowRedirect IPC from the renderer, it restarts with |
| // the new URL. |
| |
| void WorkerScriptLoader::FollowRedirect( |
| network::HttpRequestHeadersUpdateParams headers_update_params, |
| const std::optional<GURL>& new_url) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| DCHECK(!new_url.has_value()) << "Redirect with modified URL was not " |
| "supported yet. crbug.com/845683"; |
| CHECK(redirect_info_, base::NotFatalUntil::M160); |
| |
| // |should_clear_upload| is unused because there is no body anyway. |
| CHECK(!resource_request_.request_body, base::NotFatalUntil::M160); |
| bool should_clear_upload = false; |
| net::RedirectUtil::UpdateHttpRequest( |
| resource_request_.url, resource_request_.method, *redirect_info_, |
| headers_update_params.removed_headers, |
| headers_update_params.modified_headers, &resource_request_.headers, |
| &should_clear_upload); |
| resource_request_.cors_exempt_headers.MergeFrom( |
| headers_update_params.modified_cors_exempt_headers); |
| for (const std::string& name : headers_update_params.removed_headers) { |
| resource_request_.cors_exempt_headers.RemoveHeader(name); |
| } |
| |
| 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; |
| resource_request_.referrer = GURL(redirect_info_->new_referrer); |
| resource_request_.referrer_policy = redirect_info_->new_referrer_policy; |
| |
| // Restart the request. |
| url_loader_client_receiver_.reset(); |
| redirect_info_.reset(); |
| |
| Start(); |
| } |
| |
| // Below we make a small effort to support the other URLLoader functions by |
| // forwarding to the current |url_loader_| if any, but don't bother queuing |
| // state or propagating state to a new URLLoader upon redirect. |
| void WorkerScriptLoader::SetPriority(net::RequestPriority priority, |
| int32_t intra_priority_value) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| if (url_loader_) |
| url_loader_->SetPriority(priority, intra_priority_value); |
| } |
| // URLLoader end -------------------------------------------------------------- |
| |
| // URLLoaderClient ------------------------------------------------------------ |
| // This class forwards any client messages to the outer client in the renderer. |
| // Additionally, on redirects it saves the redirect info so if the renderer |
| // calls FollowRedirect(), it can do so. |
| |
| void WorkerScriptLoader::OnReceiveEarlyHints( |
| network::mojom::EarlyHintsPtr early_hints) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| // Early Hints are only supported for HTTP/HTTPS requests. |
| if (!resource_request_.url.SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| client_->OnReceiveEarlyHints(std::move(early_hints)); |
| } |
| |
| void WorkerScriptLoader::OnReceiveResponse( |
| network::mojom::URLResponseHeadPtr response_head, |
| mojo::ScopedDataPipeConsumerHandle body, |
| std::optional<mojo_base::BigBuffer> cached_metadata) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| if (resource_request_.url.SchemeIsBlob() && response_head) { |
| // A blob URL is loaded by a renderer-hosted BlobURLLoader. A blob URL |
| // request is never handled by a service worker or a network socket. |
| // Sanitize service worker and network address space fields to ensure |
| // robustness against invalid input and maintain expected specifications. |
| response_head->was_fetched_via_service_worker = false; |
| response_head->url_list_via_service_worker.clear(); |
| response_head->service_worker_response_source = |
| network::mojom::FetchResponseSource::kUnspecified; |
| response_head->initial_service_worker_status.reset(); |
| response_head->service_worker_router_info.reset(); |
| response_head->client_address_space = |
| network::mojom::IPAddressSpace::kUnknown; |
| response_head->response_address_space = |
| network::mojom::IPAddressSpace::kUnknown; |
| response_head->remote_endpoint = net::IPEndPoint(); |
| response_head->was_fetched_via_cache = false; |
| response_head->is_validated = false; |
| } |
| client_->OnReceiveResponse(std::move(response_head), std::move(body), |
| std::move(cached_metadata)); |
| } |
| |
| void WorkerScriptLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr response_head) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| |
| if (resource_request_.url.SchemeIsBlob()) { |
| // Loading a blob URL never produces a redirect. |
| complete_status_ = |
| network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT); |
| CommitCompleted(); |
| return; |
| } |
| |
| if (!IsSafeRedirectTarget(resource_request_.url, redirect_info.new_url)) { |
| complete_status_ = |
| network::URLLoaderCompletionStatus(net::ERR_UNSAFE_REDIRECT); |
| CommitCompleted(); |
| return; |
| } |
| |
| if (--redirect_limit_ == 0) { |
| complete_status_ = |
| network::URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS); |
| CommitCompleted(); |
| return; |
| } |
| |
| redirect_info_ = redirect_info; |
| client_->OnReceiveRedirect(redirect_info, std::move(response_head)); |
| } |
| |
| void WorkerScriptLoader::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback ack_callback) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| client_->OnUploadProgress(current_position, total_size, |
| std::move(ack_callback)); |
| } |
| |
| void WorkerScriptLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| network::RecordOnTransferSizeUpdatedUMA( |
| network::OnTransferSizeUpdatedFrom::kWorkerScriptLoader); |
| client_->OnTransferSizeUpdated(transfer_size_diff); |
| } |
| |
| void WorkerScriptLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| complete_status_ = status; |
| switch (state_) { |
| case State::kInitial: |
| // Don't wait for `WorkerScriptFetcher::callback_` on failure. |
| if (status.error_code != net::OK) { |
| break; |
| } |
| state_ = State::kOnCompleteCalled; |
| return; |
| case State::kFetcherCallbackCalled: |
| break; |
| case State::kOnCompleteCalled: |
| case State::kCompleted: |
| NOTREACHED(); |
| } |
| CommitCompleted(); |
| } |
| |
| // URLLoaderClient end --------------------------------------------------------- |
| |
| void WorkerScriptLoader::OnFetcherCallbackCalled() { |
| switch (state_) { |
| case State::kInitial: |
| state_ = State::kFetcherCallbackCalled; |
| break; |
| case State::kOnCompleteCalled: |
| CHECK(complete_status_); |
| CHECK_EQ(complete_status_->error_code, net::OK); |
| CommitCompleted(); |
| break; |
| case State::kCompleted: |
| // `CommitCompleted()` is already called with a failure and thus safely |
| // ignore the fetcher callback notification. |
| break; |
| case State::kFetcherCallbackCalled: |
| NOTREACHED(); |
| } |
| } |
| |
| // `CommitCompleted()` with `net::OK` must not be called before |
| // `WorkerScriptFetcher::callback_` to ensure the order: |
| // 1. `ServiceWorkerContainerHost` pipes are passed to the renderer process |
| // inside `WorkerScriptFetcher::callback_` |
| // 2. `ServiceWorkerClient::SetExecutionReady()` is called inside |
| // `WorkerScriptLoader::CommitCompleted()` |
| // 3. `client_->OnComplete()` is called which eventually triggers worker |
| // top-level script evaluation on the renderer process. |
| // (Note that non-OK `CommitCompleted()` can be called without 1 or 3) |
| void WorkerScriptLoader::CommitCompleted() { |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M160); |
| CHECK_NE(state_, State::kCompleted); |
| CHECK(complete_status_); |
| state_ = State::kCompleted; |
| |
| if (complete_status_->error_code == net::OK && service_worker_handle_ && |
| service_worker_handle_->service_worker_client()) { |
| service_worker_handle_->service_worker_client()->SetExecutionReady(); |
| } |
| |
| client_->OnComplete(*complete_status_); |
| |
| // We're done. Ensure we no longer send messages to our client, and no longer |
| // talk to the loader we're a client of. |
| client_.reset(); |
| url_loader_client_receiver_.reset(); |
| url_loader_.reset(); |
| } |
| |
| } // namespace content |