blob: f2c05dc434e47b52e685f72ad11e641c55d99300 [file] [log] [blame]
// Copyright 2014 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_write_to_cache_job.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/service_worker/service_worker_cache_writer.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_disk_cache.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/common/net/url_request_service_worker_data.h"
#include "content/common/service_worker/service_worker_types.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/http/http_network_session.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_status.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "third_party/blink/public/web/web_console_message.h"
namespace content {
namespace {
const char kKilledError[] = "The request to fetch the script was interrupted.";
const char kClientAuthenticationError[] =
"Client authentication was required to fetch the script.";
bool ShouldIgnoreSSLError(net::URLRequest* request) {
const net::HttpNetworkSession::Params* session_params =
request->context()->GetNetworkSessionParams();
if (session_params && session_params->ignore_certificate_errors)
return true;
bool allow_localhost = base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAllowInsecureLocalhost);
if (allow_localhost && net::IsLocalhost(request->url()))
return true;
return false;
}
} // namespace
const net::Error ServiceWorkerWriteToCacheJob::kIdenticalScriptError =
net::ERR_FILE_EXISTS;
ServiceWorkerWriteToCacheJob::ServiceWorkerWriteToCacheJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate,
ResourceType resource_type,
base::WeakPtr<ServiceWorkerContextCore> context,
ServiceWorkerVersion* version,
int extra_load_flags,
int64_t resource_id,
int64_t incumbent_resource_id)
: net::URLRequestJob(request, network_delegate),
resource_type_(resource_type),
context_(context),
url_(request->url()),
resource_id_(resource_id),
incumbent_resource_id_(incumbent_resource_id),
version_(version),
weak_factory_(this) {
DCHECK(version_);
DCHECK(resource_type_ == RESOURCE_TYPE_SCRIPT ||
(resource_type_ == RESOURCE_TYPE_SERVICE_WORKER &&
version_->script_url() == url_));
InitNetRequest(extra_load_flags);
}
ServiceWorkerWriteToCacheJob::~ServiceWorkerWriteToCacheJob() {
Kill();
DCHECK_EQ(did_notify_started_, did_notify_finished_);
}
void ServiceWorkerWriteToCacheJob::Start() {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ServiceWorkerWriteToCacheJob::StartAsync,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerWriteToCacheJob::StartAsync() {
TRACE_EVENT_ASYNC_BEGIN1("ServiceWorker",
"ServiceWorkerWriteToCacheJob::ExecutingJob",
this,
"URL", request_->url().spec());
if (!context_) {
// NotifyStartError is not safe to call synchronously in Start().
NotifyStartError(
net::URLRequestStatus(net::URLRequestStatus::FAILED, net::ERR_FAILED));
return;
}
// Create response readers only when we have to do the byte-for-byte check.
std::unique_ptr<ServiceWorkerResponseReader> compare_reader;
std::unique_ptr<ServiceWorkerResponseReader> copy_reader;
if (ShouldByteForByteCheck()) {
compare_reader =
context_->storage()->CreateResponseReader(incumbent_resource_id_);
copy_reader =
context_->storage()->CreateResponseReader(incumbent_resource_id_);
}
cache_writer_ = std::make_unique<ServiceWorkerCacheWriter>(
std::move(compare_reader), std::move(copy_reader),
context_->storage()->CreateResponseWriter(resource_id_),
false /* pause_when_not_identical */);
version_->script_cache_map()->NotifyStartedCaching(url_, resource_id_);
did_notify_started_ = true;
StartNetRequest();
}
void ServiceWorkerWriteToCacheJob::Kill() {
if (has_been_killed_)
return;
weak_factory_.InvalidateWeakPtrs();
has_been_killed_ = true;
net_request_.reset();
if (did_notify_started_) {
net::Error error = NotifyFinishedCaching(net::ERR_ABORTED, kKilledError);
DCHECK_EQ(net::ERR_ABORTED, error);
}
writer_.reset();
context_.reset();
net::URLRequestJob::Kill();
}
net::LoadState ServiceWorkerWriteToCacheJob::GetLoadState() const {
if (writer_ && writer_->IsWritePending())
return net::LOAD_STATE_WAITING_FOR_APPCACHE;
if (net_request_)
return net_request_->GetLoadState().state;
return net::LOAD_STATE_IDLE;
}
bool ServiceWorkerWriteToCacheJob::GetCharset(std::string* charset) {
if (!http_info())
return false;
return http_info()->headers->GetCharset(charset);
}
bool ServiceWorkerWriteToCacheJob::GetMimeType(std::string* mime_type) const {
if (!http_info())
return false;
return http_info()->headers->GetMimeType(mime_type);
}
void ServiceWorkerWriteToCacheJob::GetResponseInfo(
net::HttpResponseInfo* info) {
if (!http_info())
return;
*info = *http_info();
}
void ServiceWorkerWriteToCacheJob::SetExtraRequestHeaders(
const net::HttpRequestHeaders& headers) {
std::string value;
DCHECK(!headers.GetHeader(net::HttpRequestHeaders::kRange, &value));
net_request_->SetExtraRequestHeaders(headers);
}
int ServiceWorkerWriteToCacheJob::ReadRawData(net::IOBuffer* buf,
int buf_size) {
int rv = ReadNetData(buf, buf_size);
if (rv == net::ERR_IO_PENDING)
return net::ERR_IO_PENDING;
if (rv < 0) {
net::Error error = static_cast<net::Error>(rv);
error = NotifyFinishedCaching(error, kServiceWorkerFetchScriptError);
DCHECK_EQ(rv, error);
return error;
}
return HandleNetData(rv);
}
const net::HttpResponseInfo* ServiceWorkerWriteToCacheJob::http_info() const {
return http_info_.get();
}
void ServiceWorkerWriteToCacheJob::InitNetRequest(
int extra_load_flags) {
DCHECK(request());
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("service_worker_write_to_cache_job",
R"(
semantics {
sender: "ServiceWorker System"
description:
"When a ServiceWorker is registered, its script and immediate "
"imports are cached for performance and offline access. The "
"resources are periodically updated."
trigger:
"User visits a site which registers a ServiceWorker."
data: "None"
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 {
DefaultCookiesSetting {
policy_options {mode: MANDATORY}
DefaultCookiesSetting: 2
}
}
})");
net_request_ = request()->context()->CreateRequest(
request()->url(), request()->priority(), this, traffic_annotation);
net_request_->set_site_for_cookies(request()->site_for_cookies());
net_request_->set_initiator(request()->initiator());
net_request_->SetReferrer(request()->referrer());
net_request_->SetUserData(URLRequestServiceWorkerData::kUserDataKey,
std::make_unique<URLRequestServiceWorkerData>());
if (extra_load_flags)
net_request_->SetLoadFlags(net_request_->load_flags() | extra_load_flags);
if (resource_type_ == RESOURCE_TYPE_SERVICE_WORKER) {
// This will get copied into net_request_ when URLRequest::StartJob calls
// ServiceWorkerWriteToCacheJob::SetExtraRequestHeaders.
request()->SetExtraRequestHeaderByName("Service-Worker", "script", true);
}
}
void ServiceWorkerWriteToCacheJob::StartNetRequest() {
TRACE_EVENT_ASYNC_STEP_INTO0("ServiceWorker",
"ServiceWorkerWriteToCacheJob::ExecutingJob",
this,
"NetRequest");
net_request_->Start(); // We'll continue in OnResponseStarted.
}
int ServiceWorkerWriteToCacheJob::ReadNetData(net::IOBuffer* buf,
int buf_size) {
DCHECK_GT(buf_size, 0);
io_buffer_ = buf;
io_buffer_bytes_ = 0;
return net_request_->Read(buf, buf_size);
}
void ServiceWorkerWriteToCacheJob::OnReceivedRedirect(
net::URLRequest* request,
const net::RedirectInfo& redirect_info,
bool* defer_redirect) {
DCHECK_EQ(net_request_.get(), request);
TRACE_EVENT0("ServiceWorker",
"ServiceWorkerWriteToCacheJob::OnReceivedRedirect");
// Script resources can't redirect.
NotifyStartErrorHelper(net::ERR_UNSAFE_REDIRECT, kServiceWorkerRedirectError);
}
void ServiceWorkerWriteToCacheJob::OnAuthRequired(
net::URLRequest* request,
net::AuthChallengeInfo* auth_info) {
DCHECK_EQ(net_request_.get(), request);
TRACE_EVENT0("ServiceWorker",
"ServiceWorkerWriteToCacheJob::OnAuthRequired");
// TODO(michaeln): Pass this thru to our jobs client.
NotifyStartErrorHelper(net::ERR_FAILED, kClientAuthenticationError);
}
void ServiceWorkerWriteToCacheJob::OnCertificateRequested(
net::URLRequest* request,
net::SSLCertRequestInfo* cert_request_info) {
DCHECK_EQ(net_request_.get(), request);
TRACE_EVENT0("ServiceWorker",
"ServiceWorkerWriteToCacheJob::OnCertificateRequested");
// TODO(michaeln): Pass this thru to our jobs client.
// see NotifyCertificateRequested.
NotifyStartErrorHelper(net::ERR_FAILED, kClientAuthenticationError);
}
void ServiceWorkerWriteToCacheJob::OnSSLCertificateError(
net::URLRequest* request,
const net::SSLInfo& ssl_info,
bool fatal) {
DCHECK_EQ(net_request_.get(), request);
TRACE_EVENT0("ServiceWorker",
"ServiceWorkerWriteToCacheJob::OnSSLCertificateError");
if (ShouldIgnoreSSLError(request)) {
request->ContinueDespiteLastError();
} else {
NotifyStartErrorHelper(
net::Error(net::MapCertStatusToNetError(ssl_info.cert_status)),
kServiceWorkerSSLError);
}
}
void ServiceWorkerWriteToCacheJob::OnResponseStarted(net::URLRequest* request,
int net_error) {
DCHECK_NE(net::ERR_IO_PENDING, net_error);
DCHECK_EQ(net_request_.get(), request);
if (net_error != net::OK) {
net::Error error = static_cast<net::Error>(net_error);
NotifyStartErrorHelper(error, kServiceWorkerFetchScriptError);
return;
}
if (request->GetResponseCode() / 100 != 2) {
std::string error_message = base::StringPrintf(
kServiceWorkerBadHTTPResponseError, request->GetResponseCode());
NotifyStartErrorHelper(net::ERR_INVALID_RESPONSE, error_message);
// TODO(michaeln): Instead of error'ing immediately, send the net
// response to our consumer, just don't cache it?
return;
}
// OnSSLCertificateError is not called when the HTTPS connection is reused.
// So we check cert_status here.
if (net::IsCertStatusError(request->ssl_info().cert_status) &&
!ShouldIgnoreSSLError(request)) {
NotifyStartErrorHelper(net::Error(net::MapCertStatusToNetError(
request->ssl_info().cert_status)),
kServiceWorkerSSLError);
return;
}
if (resource_type_ == RESOURCE_TYPE_SERVICE_WORKER) {
std::string mime_type;
request->GetMimeType(&mime_type);
if (!blink::IsSupportedJavascriptMimeType(mime_type)) {
std::string error_message =
mime_type.empty() ? kServiceWorkerNoMIMEError
: base::StringPrintf(kServiceWorkerBadMIMEError,
mime_type.c_str());
NotifyStartErrorHelper(net::ERR_INSECURE_RESPONSE, error_message);
return;
}
if (!CheckPathRestriction(request))
return;
version_->SetMainScriptHttpResponseInfo(net_request_->response_info());
}
if (net_request_->response_info().network_accessed &&
!(net_request_->response_info().was_cached)) {
version_->embedded_worker()->OnNetworkAccessedForScriptLoad();
}
http_info_ =
std::make_unique<net::HttpResponseInfo>(net_request_->response_info());
scoped_refptr<HttpResponseInfoIOBuffer> info_buffer =
base::MakeRefCounted<HttpResponseInfoIOBuffer>(
std::make_unique<net::HttpResponseInfo>(
net_request_->response_info()));
net::Error error = cache_writer_->MaybeWriteHeaders(
info_buffer.get(),
base::BindOnce(&ServiceWorkerWriteToCacheJob::OnWriteHeadersComplete,
weak_factory_.GetWeakPtr()));
if (error == net::ERR_IO_PENDING)
return;
OnWriteHeadersComplete(error);
}
void ServiceWorkerWriteToCacheJob::OnWriteHeadersComplete(net::Error error) {
DCHECK_NE(net::ERR_IO_PENDING, error);
if (error != net::OK) {
ServiceWorkerMetrics::CountWriteResponseResult(
ServiceWorkerMetrics::WRITE_HEADERS_ERROR);
NotifyStartError(net::URLRequestStatus::FromError(error));
return;
}
NotifyHeadersComplete();
}
void ServiceWorkerWriteToCacheJob::OnWriteDataComplete(net::Error error) {
DCHECK_NE(net::ERR_IO_PENDING, error);
if (io_buffer_bytes_ == 0)
error = NotifyFinishedCaching(error, "");
if (error != net::OK) {
ServiceWorkerMetrics::CountWriteResponseResult(
ServiceWorkerMetrics::WRITE_DATA_ERROR);
ReadRawDataComplete(error);
return;
}
ServiceWorkerMetrics::CountWriteResponseResult(
ServiceWorkerMetrics::WRITE_OK);
ReadRawDataComplete(io_buffer_bytes_);
}
void ServiceWorkerWriteToCacheJob::OnReadCompleted(net::URLRequest* request,
int bytes_read) {
DCHECK_NE(net::ERR_IO_PENDING, bytes_read);
DCHECK_EQ(net_request_.get(), request);
int result;
if (bytes_read < 0) {
net::Error error = static_cast<net::Error>(bytes_read);
result = NotifyFinishedCaching(error, kServiceWorkerFetchScriptError);
} else {
result = HandleNetData(bytes_read);
}
// ReadRawDataComplete will be called in OnWriteDataComplete, so return early.
if (result == net::ERR_IO_PENDING)
return;
ReadRawDataComplete(result);
}
bool ServiceWorkerWriteToCacheJob::CheckPathRestriction(
net::URLRequest* request) {
std::string service_worker_allowed;
const net::HttpResponseHeaders* headers = request->response_headers();
bool has_header = headers->EnumerateHeader(nullptr, kServiceWorkerAllowed,
&service_worker_allowed);
std::string error_message;
if (!ServiceWorkerUtils::IsPathRestrictionSatisfied(
version_->scope(), url_,
has_header ? &service_worker_allowed : nullptr, &error_message)) {
NotifyStartErrorHelper(net::ERR_INSECURE_RESPONSE, error_message);
return false;
}
return true;
}
int ServiceWorkerWriteToCacheJob::HandleNetData(int bytes_read) {
io_buffer_bytes_ = bytes_read;
net::Error error = cache_writer_->MaybeWriteData(
io_buffer_.get(), bytes_read,
base::BindOnce(&ServiceWorkerWriteToCacheJob::OnWriteDataComplete,
weak_factory_.GetWeakPtr()));
// In case of ERR_IO_PENDING, this logic is done in OnWriteDataComplete.
if (error != net::ERR_IO_PENDING && bytes_read == 0) {
error = NotifyFinishedCaching(error, std::string());
}
return error == net::OK ? bytes_read : error;
}
void ServiceWorkerWriteToCacheJob::NotifyStartErrorHelper(
net::Error net_error,
const std::string& status_message) {
NotifyFinishedCaching(net_error, status_message);
NotifyStartError(net::URLRequestStatus::FromError(net_error));
}
net::Error ServiceWorkerWriteToCacheJob::NotifyFinishedCaching(
net::Error net_error,
const std::string& status_message) {
DCHECK_NE(net::ERR_IO_PENDING, net_error);
if (did_notify_finished_)
return net_error;
int size = -1;
if (net_error != net::OK) {
// AddMessageToConsole must be called before this job notifies that an error
// occurred because the worker stops soon after receiving the error
// response.
version_->embedded_worker()->AddMessageToConsole(
blink::WebConsoleMessage::kLevelError,
status_message.empty() ? kServiceWorkerFetchScriptError
: status_message);
} else {
size = cache_writer_->bytes_written();
}
// If all the calls to MaybeWriteHeaders/MaybeWriteData succeeded, but the
// incumbent entry wasn't actually replaced because the new entry was
// equivalent, the new version didn't actually install because it already
// exists.
if (net_error == net::OK && !cache_writer_->did_replace()) {
version_->SetStartWorkerStatusCode(
blink::ServiceWorkerStatusCode::kErrorExists);
version_->script_cache_map()->NotifyFinishedCaching(
url_, size, kIdenticalScriptError, std::string());
} else {
version_->script_cache_map()->NotifyFinishedCaching(url_, size, net_error,
status_message);
}
did_notify_finished_ = true;
return net_error;
}
bool ServiceWorkerWriteToCacheJob::ShouldByteForByteCheck() const {
return incumbent_resource_id_ != kInvalidServiceWorkerResourceId &&
version_->pause_after_download();
}
} // namespace content