| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/service_worker/service_worker_single_script_update_checker.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/strings/stringprintf.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "content/browser/appcache/appcache_response.h" |
| #include "content/browser/service_worker/service_worker_cache_writer.h" |
| #include "content/common/service_worker/service_worker_utils.h" |
| #include "content/public/common/resource_type.h" |
| #include "mojo/public/cpp/system/simple_watcher.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/load_flags.h" |
| #include "services/network/public/cpp/net_adapters.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| |
| // TODO(momohatt): Use ServiceWorkerMetrics for UMA. |
| |
| namespace { |
| |
| constexpr net::NetworkTrafficAnnotationTag kUpdateCheckTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("service_worker_update_checker", |
| R"( |
| semantics { |
| sender: "ServiceWorker System" |
| description: |
| "This request is issued by an update check to fetch the content of " |
| "the new scripts." |
| trigger: |
| "ServiceWorker's update logic, which is triggered by a navigation to a " |
| "site controlled by a service worker." |
| data: |
| "No body. 'Service-Worker: script' header is attached when it's the " |
| "main worker script. Requests may include cookies and credentials." |
| destination: WEBSITE |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "Users can control this feature via the 'Cookies' setting under " |
| "'Privacy, Content settings'. If cookies are disabled for a single " |
| "site, serviceworkers are disabled for the site only. If they are " |
| "totally disabled, all serviceworker requests will be stopped." |
| chrome_policy { |
| URLBlacklist { |
| URLBlacklist: { entries: '*' } |
| } |
| } |
| chrome_policy { |
| URLWhitelist { |
| URLWhitelist { } |
| } |
| } |
| } |
| comments: |
| "Chrome would be unable to update service workers without this type of " |
| "request. Using either URLBlacklist or URLWhitelist policies (or a " |
| "combination of both) limits the scope of these requests." |
| )"); |
| |
| } // namespace |
| |
| namespace content { |
| |
| // This is for debugging https://crbug.com/959627. |
| // The purpose is to see where the IOBuffer comes from by checking |__vfptr|. |
| class ServiceWorkerSingleScriptUpdateChecker::WrappedIOBuffer |
| : public net::WrappedIOBuffer { |
| public: |
| WrappedIOBuffer(const char* data) : net::WrappedIOBuffer(data) {} |
| |
| private: |
| ~WrappedIOBuffer() override = default; |
| |
| // This is to make sure that the vtable is not merged with other classes. |
| virtual void dummy() { NOTREACHED(); } |
| }; |
| |
| ServiceWorkerSingleScriptUpdateChecker::ServiceWorkerSingleScriptUpdateChecker( |
| const GURL& url, |
| bool is_main_script, |
| const GURL& scope, |
| bool force_bypass_cache, |
| blink::mojom::ServiceWorkerUpdateViaCache update_via_cache, |
| base::TimeDelta time_since_last_check, |
| scoped_refptr<network::SharedURLLoaderFactory> loader_factory, |
| std::unique_ptr<ServiceWorkerResponseReader> compare_reader, |
| std::unique_ptr<ServiceWorkerResponseReader> copy_reader, |
| std::unique_ptr<ServiceWorkerResponseWriter> writer, |
| ResultCallback callback) |
| : script_url_(url), |
| is_main_script_(is_main_script), |
| scope_(scope), |
| force_bypass_cache_(force_bypass_cache), |
| update_via_cache_(update_via_cache), |
| time_since_last_check_(time_since_last_check), |
| network_client_binding_(this), |
| network_watcher_(FROM_HERE, |
| mojo::SimpleWatcher::ArmingPolicy::MANUAL, |
| base::SequencedTaskRunnerHandle::Get()), |
| callback_(std::move(callback)), |
| weak_factory_(this) { |
| network::ResourceRequest resource_request; |
| resource_request.url = url; |
| resource_request.resource_type = static_cast<int>( |
| is_main_script ? ResourceType::kServiceWorker : ResourceType::kScript); |
| resource_request.do_not_prompt_for_login = true; |
| if (is_main_script_) |
| resource_request.headers.SetHeader("Service-Worker", "script"); |
| |
| if (ServiceWorkerUtils::ShouldValidateBrowserCacheForScript( |
| is_main_script_, force_bypass_cache_, update_via_cache_, |
| time_since_last_check_)) { |
| resource_request.load_flags |= net::LOAD_VALIDATE_CACHE; |
| } |
| |
| cache_writer_ = ServiceWorkerCacheWriter::CreateForComparison( |
| std::move(compare_reader), std::move(copy_reader), std::move(writer), |
| true /* pause_when_not_identical */); |
| |
| network::mojom::URLLoaderClientPtr network_client; |
| network_client_binding_.Bind(mojo::MakeRequest(&network_client)); |
| |
| loader_factory->CreateLoaderAndStart( |
| mojo::MakeRequest(&network_loader_), -1 /* routing_id */, |
| -1 /* request_id */, network::mojom::kURLLoadOptionNone, resource_request, |
| std::move(network_client), |
| net::MutableNetworkTrafficAnnotationTag(kUpdateCheckTrafficAnnotation)); |
| DCHECK_EQ(network_loader_state_, |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kNotStarted); |
| network_loader_state_ = |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kLoadingHeader; |
| } |
| |
| ServiceWorkerSingleScriptUpdateChecker:: |
| ~ServiceWorkerSingleScriptUpdateChecker() = default; |
| |
| // URLLoaderClient override ---------------------------------------------------- |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnReceiveResponse( |
| const network::ResourceResponseHead& response_head) { |
| DCHECK_EQ(network_loader_state_, |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kLoadingHeader); |
| |
| // We don't have complete info here, but fill in what we have now. |
| // At least we need headers and SSL info. |
| auto response_info = std::make_unique<net::HttpResponseInfo>(); |
| response_info->headers = response_head.headers; |
| if (response_head.ssl_info.has_value()) |
| response_info->ssl_info = *response_head.ssl_info; |
| response_info->was_fetched_via_spdy = response_head.was_fetched_via_spdy; |
| response_info->was_alpn_negotiated = response_head.was_alpn_negotiated; |
| response_info->alpn_negotiated_protocol = |
| response_head.alpn_negotiated_protocol; |
| response_info->connection_info = response_head.connection_info; |
| response_info->remote_endpoint = response_head.remote_endpoint; |
| |
| network_loader_state_ = |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kWaitingForBody; |
| network_accessed_ = response_head.network_accessed; |
| |
| if (response_head.headers->response_code() / 100 != 2) { |
| std::string error_message = |
| base::StringPrintf(kServiceWorkerBadHTTPResponseError, |
| response_head.headers->response_code()); |
| Fail(blink::ServiceWorkerStatusCode::kErrorNetwork, error_message); |
| return; |
| } |
| |
| // Check the certificate error. |
| if (net::IsCertStatusError(response_head.cert_status) && |
| !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kIgnoreCertificateErrors)) { |
| Fail(blink::ServiceWorkerStatusCode::kErrorSecurity, |
| kServiceWorkerSSLError); |
| return; |
| } |
| |
| if (!blink::IsSupportedJavascriptMimeType(response_head.mime_type)) { |
| std::string error_message = |
| response_head.mime_type.empty() |
| ? kServiceWorkerNoMIMEError |
| : base::StringPrintf(kServiceWorkerBadMIMEError, |
| response_head.mime_type.c_str()); |
| Fail(blink::ServiceWorkerStatusCode::kErrorSecurity, error_message); |
| return; |
| } |
| |
| // Check the path restriction defined in the spec: |
| // https://w3c.github.io/ServiceWorker/#service-worker-script-response |
| // Only main script needs the following check. |
| if (is_main_script_) { |
| std::string service_worker_allowed; |
| bool has_header = response_head.headers->EnumerateHeader( |
| nullptr, kServiceWorkerAllowed, &service_worker_allowed); |
| std::string error_message; |
| if (!ServiceWorkerUtils::IsPathRestrictionSatisfied( |
| scope_, script_url_, has_header ? &service_worker_allowed : nullptr, |
| &error_message)) { |
| Fail(blink::ServiceWorkerStatusCode::kErrorSecurity, error_message); |
| return; |
| } |
| } |
| |
| WriteHeaders( |
| base::MakeRefCounted<HttpResponseInfoIOBuffer>(std::move(response_info))); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| const network::ResourceResponseHead& response_head) { |
| // TODO(momohatt): Raise error and terminate the update check here, like |
| // ServiceWorkerNewScriptLoader does. |
| NOTIMPLEMENTED(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnUploadProgress( |
| int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback ack_callback) { |
| // The network request for update checking shouldn't have upload data. |
| NOTREACHED(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnReceiveCachedMetadata( |
| mojo_base::BigBuffer data) {} |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnTransferSizeUpdated( |
| int32_t transfer_size_diff) {} |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle consumer) { |
| DCHECK_EQ(network_loader_state_, |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kWaitingForBody); |
| |
| network_consumer_ = std::move(consumer); |
| network_loader_state_ = |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kLoadingBody; |
| MaybeStartNetworkConsumerHandleWatcher(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnComplete( |
| const network::URLLoaderCompletionStatus& status) { |
| ServiceWorkerNewScriptLoader::NetworkLoaderState previous_loader_state = |
| network_loader_state_; |
| network_loader_state_ = |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kCompleted; |
| if (status.error_code != net::OK) { |
| Fail(blink::ServiceWorkerStatusCode::kErrorNetwork, |
| kServiceWorkerFetchScriptError); |
| return; |
| } |
| |
| DCHECK( |
| previous_loader_state == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kWaitingForBody || |
| previous_loader_state == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kLoadingBody); |
| |
| // Response body is empty. |
| if (previous_loader_state == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kWaitingForBody) { |
| DCHECK_EQ(body_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kNotStarted); |
| body_writer_state_ = ServiceWorkerNewScriptLoader::WriterState::kCompleted; |
| switch (header_writer_state_) { |
| case ServiceWorkerNewScriptLoader::WriterState::kNotStarted: |
| NOTREACHED() |
| << "Response header should be received before OnComplete()"; |
| break; |
| case ServiceWorkerNewScriptLoader::WriterState::kWriting: |
| // Wait until it's written. OnWriteHeadersComplete() will call |
| // Finish(). |
| return; |
| case ServiceWorkerNewScriptLoader::WriterState::kCompleted: |
| DCHECK(!network_consumer_.is_valid()); |
| // Compare the cached data with an empty data to notify |cache_writer_| |
| // of the end of the comparison. |
| CompareData(nullptr /* pending_buffer */, 0 /* bytes_available */); |
| break; |
| } |
| } |
| |
| // Response body exists. |
| if (previous_loader_state == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kLoadingBody) { |
| switch (body_writer_state_) { |
| case ServiceWorkerNewScriptLoader::WriterState::kNotStarted: |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kWriting); |
| return; |
| case ServiceWorkerNewScriptLoader::WriterState::kWriting: |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kCompleted); |
| return; |
| case ServiceWorkerNewScriptLoader::WriterState::kCompleted: |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kCompleted); |
| Succeed(Result::kIdentical); |
| return; |
| } |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| void ServiceWorkerSingleScriptUpdateChecker::WriteHeaders( |
| scoped_refptr<HttpResponseInfoIOBuffer> info_buffer) { |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kNotStarted); |
| header_writer_state_ = ServiceWorkerNewScriptLoader::WriterState::kWriting; |
| |
| // Pass the header to the cache_writer_. This is written to the storage when |
| // the body had changes. |
| net::Error error = cache_writer_->MaybeWriteHeaders( |
| info_buffer.get(), |
| base::BindOnce( |
| &ServiceWorkerSingleScriptUpdateChecker::OnWriteHeadersComplete, |
| weak_factory_.GetWeakPtr())); |
| if (error == net::ERR_IO_PENDING) { |
| // OnWriteHeadersComplete() will be called asynchronously. |
| return; |
| } |
| // MaybeWriteHeaders() doesn't run the callback if it finishes synchronously, |
| // so explicitly call it here. |
| OnWriteHeadersComplete(error); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnWriteHeadersComplete( |
| net::Error error) { |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kWriting); |
| DCHECK_NE(error, net::ERR_IO_PENDING); |
| header_writer_state_ = ServiceWorkerNewScriptLoader::WriterState::kCompleted; |
| if (error != net::OK) { |
| Fail(blink::ServiceWorkerStatusCode::kErrorFailed, |
| kServiceWorkerFetchScriptError); |
| return; |
| } |
| |
| MaybeStartNetworkConsumerHandleWatcher(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker:: |
| MaybeStartNetworkConsumerHandleWatcher() { |
| if (network_loader_state_ == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kWaitingForBody) { |
| // OnStartLoadingResponseBody() or OnComplete() will continue the sequence. |
| return; |
| } |
| if (header_writer_state_ != |
| ServiceWorkerNewScriptLoader::WriterState::kCompleted) { |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kWriting); |
| // OnWriteHeadersComplete() will continue the sequence. |
| return; |
| } |
| |
| DCHECK_EQ(body_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kNotStarted); |
| body_writer_state_ = ServiceWorkerNewScriptLoader::WriterState::kWriting; |
| |
| network_watcher_.Watch( |
| network_consumer_.get(), |
| MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, |
| base::BindRepeating( |
| &ServiceWorkerSingleScriptUpdateChecker::OnNetworkDataAvailable, |
| weak_factory_.GetWeakPtr())); |
| network_watcher_.ArmOrNotify(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::OnNetworkDataAvailable( |
| MojoResult, |
| const mojo::HandleSignalsState& state) { |
| DCHECK_EQ(header_writer_state_, |
| ServiceWorkerNewScriptLoader::WriterState::kCompleted); |
| DCHECK(network_consumer_.is_valid()); |
| scoped_refptr<network::MojoToNetPendingBuffer> pending_buffer; |
| uint32_t bytes_available = 0; |
| MojoResult result = network::MojoToNetPendingBuffer::BeginRead( |
| &network_consumer_, &pending_buffer, &bytes_available); |
| switch (result) { |
| case MOJO_RESULT_OK: |
| CompareData(std::move(pending_buffer), bytes_available); |
| return; |
| case MOJO_RESULT_FAILED_PRECONDITION: |
| body_writer_state_ = |
| ServiceWorkerNewScriptLoader::WriterState::kCompleted; |
| // Closed by peer. This indicates all the data from the network service |
| // are read or there is an error. In the error case, the reason is |
| // notified via OnComplete(). |
| if (network_loader_state_ == |
| ServiceWorkerNewScriptLoader::NetworkLoaderState::kCompleted) { |
| // Compare the cached data with an empty data to notify |cache_writer_| |
| // the end of the comparison. |
| CompareData(nullptr /* pending_buffer */, 0 /* bytes_available */); |
| } |
| return; |
| case MOJO_RESULT_SHOULD_WAIT: |
| network_watcher_.ArmOrNotify(); |
| return; |
| } |
| NOTREACHED() << static_cast<int>(result); |
| } |
| |
| // |pending_buffer| is a buffer keeping a Mojo data pipe which is going to be |
| // read by a cache writer. It should be kept alive until the read is done. It's |
| // nullptr when there is no data to be read, and that means the body from the |
| // network reaches the end. In that case, |bytes_to_compare| is zero. |
| void ServiceWorkerSingleScriptUpdateChecker::CompareData( |
| scoped_refptr<network::MojoToNetPendingBuffer> pending_buffer, |
| uint32_t bytes_to_compare) { |
| DCHECK(pending_buffer || bytes_to_compare == 0); |
| auto buffer = base::MakeRefCounted<WrappedIOBuffer>( |
| pending_buffer ? pending_buffer->buffer() : nullptr); |
| |
| // Compare the network data and the stored data. |
| net::Error error = cache_writer_->MaybeWriteData( |
| buffer.get(), bytes_to_compare, |
| base::BindOnce( |
| &ServiceWorkerSingleScriptUpdateChecker::OnCompareDataComplete, |
| weak_factory_.GetWeakPtr(), pending_buffer, bytes_to_compare)); |
| |
| if (error == net::ERR_IO_PENDING && !cache_writer_->is_pausing()) { |
| // OnCompareDataComplete() will be called asynchronously. |
| return; |
| } |
| // MaybeWriteData() doesn't run the callback if it finishes synchronously, so |
| // explicitly call it here. |
| OnCompareDataComplete(std::move(pending_buffer), bytes_to_compare, error); |
| } |
| |
| // |pending_buffer| is a buffer passed from CompareData(). Please refer to the |
| // comment on CompareData(). |error| is the result of the comparison done by the |
| // cache writer (which is actually reading and not yet writing to the cache, |
| // since it's in the comparison phase). It's net::OK when the body from the |
| // network and from the disk cache are the same, net::ERR_IO_PENDING if it |
| // detects a change in the script, or other error code if something went wrong |
| // reading from the disk cache. |
| void ServiceWorkerSingleScriptUpdateChecker::OnCompareDataComplete( |
| scoped_refptr<network::MojoToNetPendingBuffer> pending_buffer, |
| uint32_t bytes_written, |
| net::Error error) { |
| DCHECK(pending_buffer || bytes_written == 0); |
| if (pending_buffer) { |
| // We consumed |bytes_written| bytes of data from the network so call |
| // CompleteRead(), regardless of what |error| is. |
| pending_buffer->CompleteRead(bytes_written); |
| network_consumer_ = pending_buffer->ReleaseHandle(); |
| } |
| |
| if (cache_writer_->is_pausing()) { |
| // |cache_writer_| can be pausing only when it finds difference between |
| // stored body and network body. |
| DCHECK_EQ(error, net::ERR_IO_PENDING); |
| Succeed(Result::kDifferent); |
| return; |
| } |
| |
| if (error != net::OK) { |
| // Something went wrong reading from the disk cache. |
| Fail(blink::ServiceWorkerStatusCode::kErrorDiskCache, |
| kServiceWorkerFetchScriptError); |
| return; |
| } |
| |
| if (bytes_written == 0) { |
| // All data has been read. If we reach here without any error, the script |
| // from the network was identical to the one in the disk cache. |
| Succeed(Result::kIdentical); |
| return; |
| } |
| |
| network_watcher_.ArmOrNotify(); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::Fail( |
| blink::ServiceWorkerStatusCode status, |
| const std::string& error_message) { |
| Finish(Result::kFailed, std::make_unique<FailureInfo>(status, error_message)); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::Succeed(Result result) { |
| DCHECK_NE(result, Result::kFailed); |
| Finish(result, nullptr); |
| } |
| |
| void ServiceWorkerSingleScriptUpdateChecker::Finish( |
| Result result, |
| std::unique_ptr<FailureInfo> failure_info) { |
| network_watcher_.Cancel(); |
| if (Result::kDifferent == result) { |
| auto paused_state = std::make_unique<PausedState>( |
| std::move(cache_writer_), std::move(network_loader_), |
| network_client_binding_.Unbind(), std::move(network_consumer_), |
| network_loader_state_, body_writer_state_); |
| std::move(callback_).Run(script_url_, result, nullptr, |
| std::move(paused_state)); |
| return; |
| } |
| |
| network_loader_.reset(); |
| network_client_binding_.Close(); |
| network_consumer_.reset(); |
| std::move(callback_).Run(script_url_, result, std::move(failure_info), |
| nullptr); |
| } |
| |
| ServiceWorkerSingleScriptUpdateChecker::PausedState::PausedState( |
| std::unique_ptr<ServiceWorkerCacheWriter> cache_writer, |
| network::mojom::URLLoaderPtr network_loader, |
| network::mojom::URLLoaderClientRequest network_client_request, |
| mojo::ScopedDataPipeConsumerHandle network_consumer, |
| ServiceWorkerNewScriptLoader::NetworkLoaderState network_loader_state, |
| ServiceWorkerNewScriptLoader::WriterState body_writer_state) |
| : cache_writer(std::move(cache_writer)), |
| network_loader(std::move(network_loader)), |
| network_client_request(std::move(network_client_request)), |
| network_consumer(std::move(network_consumer)), |
| network_loader_state(network_loader_state), |
| body_writer_state(body_writer_state) {} |
| |
| ServiceWorkerSingleScriptUpdateChecker::PausedState::~PausedState() = default; |
| |
| ServiceWorkerSingleScriptUpdateChecker::FailureInfo::FailureInfo( |
| blink::ServiceWorkerStatusCode status, |
| const std::string& error_message) |
| : status(status), error_message(error_message) {} |
| ServiceWorkerSingleScriptUpdateChecker::FailureInfo::~FailureInfo() = default; |
| |
| } // namespace content |