blob: 64eea07cbe3e2256ebfdb7bcf978a33118ecce67 [file] [log] [blame]
// Copyright 2017 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_navigation_loader.h"
#include <sstream>
#include <utility>
#include "base/optional.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/browser/url_loader_factory_getter.h"
#include "content/common/service_worker/service_worker_loader_helpers.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/resource_request_info.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
namespace content {
namespace {
std::string ComposeFetchEventResultString(
ServiceWorkerFetchDispatcher::FetchEventResult result,
const blink::mojom::FetchAPIResponse& response) {
if (result == ServiceWorkerFetchDispatcher::FetchEventResult::kShouldFallback)
return "Fallback to network";
std::stringstream stream;
stream << "Got response (status_code: " << response.status_code
<< " status_text: '" << response.status_text << "')";
return stream.str();
}
bool BodyHasNoDataPipeGetters(const network::ResourceRequestBody* body) {
if (!body)
return true;
for (const auto& elem : *body->elements()) {
if (elem.type() == network::DataElement::TYPE_DATA_PIPE)
return false;
}
return true;
}
} // namespace
// This class waits for completion of a stream response from the service worker.
// It calls ServiceWorkerNavigationLoader::CommitCompleted() upon completion of
// the response.
class ServiceWorkerNavigationLoader::StreamWaiter
: public blink::mojom::ServiceWorkerStreamCallback {
public:
StreamWaiter(
ServiceWorkerNavigationLoader* owner,
scoped_refptr<ServiceWorkerVersion> streaming_version,
blink::mojom::ServiceWorkerStreamCallbackRequest callback_request)
: owner_(owner),
streaming_version_(streaming_version),
binding_(this, std::move(callback_request)) {
streaming_version_->OnStreamResponseStarted();
binding_.set_connection_error_handler(
base::BindOnce(&StreamWaiter::OnAborted, base::Unretained(this)));
}
~StreamWaiter() override { streaming_version_->OnStreamResponseFinished(); }
// Implements mojom::ServiceWorkerStreamCallback.
void OnCompleted() override {
// Destroys |this|.
owner_->CommitCompleted(net::OK);
}
void OnAborted() override {
// Destroys |this|.
owner_->CommitCompleted(net::ERR_ABORTED);
}
private:
ServiceWorkerNavigationLoader* owner_;
scoped_refptr<ServiceWorkerVersion> streaming_version_;
mojo::Binding<blink::mojom::ServiceWorkerStreamCallback> binding_;
DISALLOW_COPY_AND_ASSIGN(StreamWaiter);
};
ServiceWorkerNavigationLoader::ServiceWorkerNavigationLoader(
NavigationLoaderInterceptor::LoaderCallback callback,
Delegate* delegate,
const network::ResourceRequest& resource_request,
scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter)
: loader_callback_(std::move(callback)),
delegate_(delegate),
resource_request_(resource_request),
url_loader_factory_getter_(std::move(url_loader_factory_getter)),
binding_(this),
weak_factory_(this) {
TRACE_EVENT_WITH_FLOW1(
"ServiceWorker",
"ServiceWorkerNavigationLoader::ServiceWorkerNavigationloader", this,
TRACE_EVENT_FLAG_FLOW_OUT, "url", resource_request_.url.spec());
DCHECK(ServiceWorkerUtils::IsMainResourceType(
static_cast<ResourceType>(resource_request.resource_type)));
response_head_.load_timing.request_start = base::TimeTicks::Now();
response_head_.load_timing.request_start_time = base::Time::Now();
}
ServiceWorkerNavigationLoader::~ServiceWorkerNavigationLoader() {
TRACE_EVENT_WITH_FLOW0(
"ServiceWorker",
"ServiceWorkerNavigationLoader::~ServiceWorkerNavigationloader", this,
TRACE_EVENT_FLAG_FLOW_IN);
};
void ServiceWorkerNavigationLoader::FallbackToNetwork() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
TRACE_EVENT_WITH_FLOW0(
"ServiceWorker", "ServiceWorkerNavigationLoader::FallbackToNetwork", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
response_type_ = ResponseType::FALLBACK_TO_NETWORK;
status_ = Status::kCompleted;
// This could be called multiple times in some cases because we simply
// call this synchronously here and don't wait for a separate async
// StartRequest cue like what URLRequestJob case does.
// TODO(kinuko): Make sure this is ok or we need to make this async.
if (loader_callback_)
std::move(loader_callback_).Run({});
}
void ServiceWorkerNavigationLoader::ForwardToServiceWorker() {
TRACE_EVENT_WITH_FLOW0(
"ServiceWorker", "ServiceWorkerNavigationLoader::ForwardToServiceWorker",
this, TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
response_type_ = ResponseType::FORWARD_TO_SERVICE_WORKER;
StartRequest();
}
bool ServiceWorkerNavigationLoader::ShouldFallbackToNetwork() {
return response_type_ == ResponseType::FALLBACK_TO_NETWORK;
}
bool ServiceWorkerNavigationLoader::ShouldForwardToServiceWorker() {
return response_type_ == ResponseType::FORWARD_TO_SERVICE_WORKER;
}
bool ServiceWorkerNavigationLoader::WasCanceled() const {
return status_ == Status::kCancelled;
}
void ServiceWorkerNavigationLoader::DetachedFromRequest() {
detached_from_request_ = true;
DeleteIfNeeded();
}
base::WeakPtr<ServiceWorkerNavigationLoader>
ServiceWorkerNavigationLoader::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void ServiceWorkerNavigationLoader::StartRequest() {
DCHECK_EQ(ResponseType::FORWARD_TO_SERVICE_WORKER, response_type_);
DCHECK_EQ(Status::kNotStarted, status_);
status_ = Status::kStarted;
TRACE_EVENT_WITH_FLOW0("ServiceWorker",
"ServiceWorkerNavigationLoader::StartRequest", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
ServiceWorkerMetrics::URLRequestJobResult result =
ServiceWorkerMetrics::REQUEST_JOB_ERROR_BAD_DELEGATE;
ServiceWorkerVersion* active_worker =
delegate_->GetServiceWorkerVersion(&result);
if (!active_worker) {
ReturnNetworkError();
return;
}
// ServiceWorkerFetchDispatcher requires a std::unique_ptr<ResourceRequest>
// so make one here.
// TODO(crbug.com/803125): Try to eliminate unnecessary copying?
auto request = std::make_unique<network::ResourceRequest>(resource_request_);
// Passing the request body over Mojo moves out the DataPipeGetter elements,
// which would mean we should clone the body like
// ServiceWorkerSubresourceLoader does. But we don't expect DataPipeGetters
// here yet: they are only created by the renderer when converting from a
// Blob, which doesn't happen for navigations. In interest of speed, just
// don't clone until proven necessary.
DCHECK(BodyHasNoDataPipeGetters(request->request_body.get()))
<< "We assumed there would be no data pipe getter elements here, but "
"there are. Add code here to clone the body before proceeding.";
// Dispatch the fetch event.
fetch_dispatcher_ = std::make_unique<ServiceWorkerFetchDispatcher>(
std::move(request), std::string() /* request_body_blob_uuid */,
0 /* request_body_blob_size */, nullptr /* request_body_blob */,
std::string() /* client_id */, active_worker,
net::NetLogWithSource() /* TODO(scottmg): net log? */,
base::BindOnce(&ServiceWorkerNavigationLoader::DidPrepareFetchEvent,
weak_factory_.GetWeakPtr(),
base::WrapRefCounted(active_worker),
active_worker->running_status()),
base::BindOnce(&ServiceWorkerNavigationLoader::DidDispatchFetchEvent,
weak_factory_.GetWeakPtr()));
did_navigation_preload_ =
fetch_dispatcher_->MaybeStartNavigationPreloadWithURLLoader(
resource_request_, url_loader_factory_getter_.get(),
base::DoNothing(/* TODO(crbug/762357): metrics? */));
response_head_.service_worker_start_time = base::TimeTicks::Now();
response_head_.load_timing.send_start = base::TimeTicks::Now();
response_head_.load_timing.send_end = base::TimeTicks::Now();
fetch_dispatcher_->Run();
}
void ServiceWorkerNavigationLoader::CommitResponseHeaders() {
DCHECK_EQ(Status::kStarted, status_);
DCHECK(url_loader_client_.is_bound());
TRACE_EVENT_WITH_FLOW2(
"ServiceWorker", "ServiceWorkerNavigationLoader::CommitResponseHeaders",
this, TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"response_code", response_head_.headers->response_code(), "status_text",
response_head_.headers->GetStatusText());
status_ = Status::kSentHeader;
url_loader_client_->OnReceiveResponse(response_head_);
}
void ServiceWorkerNavigationLoader::CommitCompleted(int error_code) {
TRACE_EVENT_WITH_FLOW1("ServiceWorker",
"ServiceWorkerNavigationLoader::CommitCompleted", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"error_code", net::ErrorToString(error_code));
DCHECK_LT(status_, Status::kCompleted);
DCHECK(url_loader_client_.is_bound());
status_ = Status::kCompleted;
// |stream_waiter_| calls this when done.
stream_waiter_.reset();
url_loader_client_->OnComplete(
network::URLLoaderCompletionStatus(error_code));
}
void ServiceWorkerNavigationLoader::ReturnNetworkError() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
TRACE_EVENT_WITH_FLOW0(
"ServiceWorker", "ServiceWorkerNavigationLoader::ReturnNetworkError",
this, TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
DCHECK(!url_loader_client_.is_bound());
DCHECK(loader_callback_);
std::move(loader_callback_)
.Run(base::BindOnce(&ServiceWorkerNavigationLoader::StartErrorResponse,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerNavigationLoader::DidPrepareFetchEvent(
scoped_refptr<ServiceWorkerVersion> version,
EmbeddedWorkerStatus initial_worker_status) {
TRACE_EVENT_WITH_FLOW1(
"ServiceWorker", "ServiceWorkerNavigationLoader::DidPrepareFetchEvent",
this, TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"initial_worker_status",
EmbeddedWorkerInstance::StatusToString(initial_worker_status));
response_head_.service_worker_ready_time = base::TimeTicks::Now();
// Note that we don't record worker preparation time in S13nServiceWorker
// path for now. If we want to measure worker preparation time we can
// calculate it from response_head_.service_worker_ready_time and
// response_head_.load_timing.request_start.
// https://crbug.com/852664
ServiceWorkerMetrics::RecordActivatedWorkerPreparationForMainFrame(
base::TimeDelta(), initial_worker_status,
version->embedded_worker()->start_situation(), did_navigation_preload_,
resource_request_.url);
}
void ServiceWorkerNavigationLoader::DidDispatchFetchEvent(
blink::ServiceWorkerStatusCode status,
ServiceWorkerFetchDispatcher::FetchEventResult fetch_result,
blink::mojom::FetchAPIResponsePtr response,
blink::mojom::ServiceWorkerStreamHandlePtr body_as_stream,
scoped_refptr<ServiceWorkerVersion> version) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
TRACE_EVENT_WITH_FLOW2(
"ServiceWorker", "ServiceWorkerNavigationLoader::DidDispatchFetchEvent",
this, TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT, "status",
blink::ServiceWorkerStatusToString(status), "result",
ComposeFetchEventResultString(fetch_result, *response));
ServiceWorkerMetrics::RecordFetchEventStatus(true /* is_main_resource */,
status);
ServiceWorkerMetrics::URLRequestJobResult result =
ServiceWorkerMetrics::REQUEST_JOB_ERROR_BAD_DELEGATE;
if (!delegate_->RequestStillValid(&result)) {
ReturnNetworkError();
return;
}
if (status != blink::ServiceWorkerStatusCode::kOk) {
delegate_->MainResourceLoadFailed();
FallbackToNetwork();
return;
}
if (fetch_result ==
ServiceWorkerFetchDispatcher::FetchEventResult::kShouldFallback) {
FallbackToNetwork();
return;
}
DCHECK_EQ(fetch_result,
ServiceWorkerFetchDispatcher::FetchEventResult::kGotResponse);
// A response with status code 0 is Blink telling us to respond with
// network error.
if (response->status_code == 0) {
ReturnNetworkError();
return;
}
std::move(loader_callback_)
.Run(base::BindOnce(&ServiceWorkerNavigationLoader::StartResponse,
weak_factory_.GetWeakPtr(), std::move(response),
version, std::move(body_as_stream)));
}
void ServiceWorkerNavigationLoader::StartResponse(
blink::mojom::FetchAPIResponsePtr response,
scoped_refptr<ServiceWorkerVersion> version,
blink::mojom::ServiceWorkerStreamHandlePtr body_as_stream,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
DCHECK(!binding_.is_bound());
DCHECK(!url_loader_client_.is_bound());
binding_.Bind(std::move(request));
binding_.set_connection_error_handler(
base::BindOnce(&ServiceWorkerNavigationLoader::OnConnectionClosed,
base::Unretained(this)));
url_loader_client_ = std::move(client);
ServiceWorkerLoaderHelpers::SaveResponseInfo(*response, &response_head_);
ServiceWorkerLoaderHelpers::SaveResponseHeaders(
response->status_code, response->status_text, response->headers,
&response_head_);
response_head_.did_service_worker_navigation_preload =
did_navigation_preload_;
response_head_.load_timing.receive_headers_end = base::TimeTicks::Now();
// Make the navigated page inherit the SSLInfo from its controller service
// worker's script. This affects the HTTPS padlock, etc, shown by the
// browser. See https://crbug.com/392409 for details about this design.
// TODO(horo): When we support mixed-content (HTTP) no-cors requests from a
// ServiceWorker, we have to check the security level of the responses.
response_head_.ssl_info = version->GetMainScriptHttpResponseInfo()->ssl_info;
// Handle a redirect response. ComputeRedirectInfo returns non-null redirect
// info if the given response is a redirect.
base::Optional<net::RedirectInfo> redirect_info =
ServiceWorkerLoaderHelpers::ComputeRedirectInfo(
resource_request_, response_head_,
response_head_.ssl_info->token_binding_negotiated);
if (redirect_info) {
TRACE_EVENT_WITH_FLOW2(
"ServiceWorker", "ServiceWorkerNavigationLoader::StartResponse", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT, "result",
"redirect", "redirect url", redirect_info->new_url.spec());
response_head_.encoded_data_length = 0;
url_loader_client_->OnReceiveRedirect(*redirect_info, response_head_);
// Our client is the navigation loader, which will start a new URLLoader for
// the redirect rather than calling FollowRedirect(), so we're done here.
status_ = Status::kCompleted;
return;
}
// We have a non-redirect response. Send the headers to the client.
CommitResponseHeaders();
// Handle a stream response body.
if (!body_as_stream.is_null() && body_as_stream->stream.is_valid()) {
TRACE_EVENT_WITH_FLOW1("ServiceWorker",
"ServiceWorkerNavigationLoader::StartResponse", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"result", "stream response");
stream_waiter_ = std::make_unique<StreamWaiter>(
this, std::move(version), std::move(body_as_stream->callback_request));
url_loader_client_->OnStartLoadingResponseBody(
std::move(body_as_stream->stream));
// StreamWaiter will call CommitCompleted() when done.
return;
}
// Handle a blob response body.
if (response->blob) {
DCHECK(response->blob->blob.is_valid());
body_as_blob_.Bind(std::move(response->blob->blob));
mojo::ScopedDataPipeConsumerHandle data_pipe;
int error = ServiceWorkerLoaderHelpers::ReadBlobResponseBody(
&body_as_blob_, resource_request_.headers,
base::BindOnce(&ServiceWorkerNavigationLoader::OnBlobReadingComplete,
weak_factory_.GetWeakPtr()),
&data_pipe);
if (error != net::OK) {
CommitCompleted(error);
return;
}
TRACE_EVENT_WITH_FLOW1("ServiceWorker",
"ServiceWorkerNavigationLoader::StartResponse", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"result", "blob response");
url_loader_client_->OnStartLoadingResponseBody(std::move(data_pipe));
// We continue in OnBlobReadingComplete().
return;
}
TRACE_EVENT_WITH_FLOW1("ServiceWorker",
"ServiceWorkerNavigationLoader::StartResponse", this,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT,
"result", "no body");
// The response has no body.
CommitCompleted(net::OK);
}
void ServiceWorkerNavigationLoader::StartErrorResponse(
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
DCHECK_EQ(Status::kStarted, status_);
DCHECK(!url_loader_client_.is_bound());
url_loader_client_ = std::move(client);
CommitCompleted(net::ERR_FAILED);
}
// URLLoader implementation----------------------------------------
void ServiceWorkerNavigationLoader::FollowRedirect(
const base::Optional<std::vector<std::string>>&
to_be_removed_request_headers,
const base::Optional<net::HttpRequestHeaders>& modified_request_headers) {
NOTIMPLEMENTED();
}
void ServiceWorkerNavigationLoader::ProceedWithResponse() {
// ServiceWorkerNavigationLoader doesn't need to wait for
// ProceedWithResponse() since it doesn't use MojoAsyncResourceHandler to load
// the resource request.
}
void ServiceWorkerNavigationLoader::SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) {
NOTIMPLEMENTED();
}
void ServiceWorkerNavigationLoader::PauseReadingBodyFromNet() {}
void ServiceWorkerNavigationLoader::ResumeReadingBodyFromNet() {}
void ServiceWorkerNavigationLoader::OnBlobReadingComplete(int net_error) {
CommitCompleted(net_error);
body_as_blob_.reset();
}
void ServiceWorkerNavigationLoader::OnConnectionClosed() {
weak_factory_.InvalidateWeakPtrs();
fetch_dispatcher_.reset();
stream_waiter_.reset();
binding_.Close();
// Cancel the request if this loader hasn't yet responded to it.
if (status_ != Status::kCompleted && status_ != Status::kCancelled) {
status_ = Status::kCancelled;
url_loader_client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_ABORTED));
}
url_loader_client_.reset();
DeleteIfNeeded();
}
void ServiceWorkerNavigationLoader::DeleteIfNeeded() {
if (!binding_.is_bound() && detached_from_request_)
delete this;
}
} // namespace content