blob: 9cf7326f0bb4388fea6ed4b0b92e979e3fe0b291 [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 "third_party/blink/renderer/modules/service_worker/fetch_respond_with_observer.h"
#include <memory>
#include <utility>
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/mojom/fetch_api.mojom-blink.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_response.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/body_stream_buffer.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/modules/service_worker/cross_origin_resource_policy_checker.h"
#include "third_party/blink/renderer/modules/service_worker/fetch_event.h"
#include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
#include "third_party/blink/renderer/modules/service_worker/wait_until_observer.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/loader/fetch/bytes_consumer.h"
#include "v8/include/v8.h"
using blink::mojom::ServiceWorkerResponseError;
namespace blink {
namespace {
// Returns the error message to let the developer know about the reason of the
// unusual failures.
const String GetMessageForResponseError(ServiceWorkerResponseError error,
const KURL& request_url) {
String error_message = "The FetchEvent for \"" + request_url.GetString() +
"\" resulted in a network error response: ";
switch (error) {
case ServiceWorkerResponseError::kPromiseRejected:
error_message = error_message + "the promise was rejected.";
break;
case ServiceWorkerResponseError::kDefaultPrevented:
error_message =
error_message +
"preventDefault() was called without calling respondWith().";
break;
case ServiceWorkerResponseError::kNoV8Instance:
error_message =
error_message +
"an object that was not a Response was passed to respondWith().";
break;
case ServiceWorkerResponseError::kResponseTypeError:
error_message = error_message +
"the promise was resolved with an error response object.";
break;
case ServiceWorkerResponseError::kResponseTypeOpaque:
error_message =
error_message +
"an \"opaque\" response was used for a request whose type "
"is not no-cors";
break;
case ServiceWorkerResponseError::kResponseTypeNotBasicOrDefault:
NOTREACHED();
break;
case ServiceWorkerResponseError::kBodyUsed:
error_message =
error_message +
"a Response whose \"bodyUsed\" is \"true\" cannot be used "
"to respond to a request.";
break;
case ServiceWorkerResponseError::kResponseTypeOpaqueForClientRequest:
error_message = error_message +
"an \"opaque\" response was used for a client request.";
break;
case ServiceWorkerResponseError::kResponseTypeOpaqueRedirect:
error_message = error_message +
"an \"opaqueredirect\" type response was used for a "
"request whose redirect mode is not \"manual\".";
break;
case ServiceWorkerResponseError::kResponseTypeCorsForRequestModeSameOrigin:
error_message = error_message +
"a \"cors\" type response was used for a request whose "
"mode is \"same-origin\".";
break;
case ServiceWorkerResponseError::kBodyLocked:
error_message = error_message +
"a Response whose \"body\" is locked cannot be used to "
"respond to a request.";
break;
case ServiceWorkerResponseError::kRedirectedResponseForNotFollowRequest:
error_message = error_message +
"a redirected response was used for a request whose "
"redirect mode is not \"follow\".";
break;
case ServiceWorkerResponseError::kDataPipeCreationFailed:
error_message = error_message + "insufficient resources.";
break;
case ServiceWorkerResponseError::kResponseBodyBroken:
error_message =
error_message + "a response body's status could not be checked.";
break;
case ServiceWorkerResponseError::kDisallowedByCorp:
error_message = error_message +
"Cross-Origin-Resource-Policy prevented from serving the "
"response to the client.";
break;
case ServiceWorkerResponseError::kUnknown:
default:
error_message = error_message + "an unexpected error occurred.";
break;
}
return error_message;
}
bool IsNavigationRequest(mojom::RequestContextFrameType frame_type) {
return frame_type != mojom::RequestContextFrameType::kNone;
}
bool IsClientRequest(mojom::RequestContextFrameType frame_type,
network::mojom::RequestDestination destination) {
return IsNavigationRequest(frame_type) ||
destination == network::mojom::RequestDestination::kSharedWorker ||
destination == network::mojom::RequestDestination::kWorker;
}
// Notifies the result of FetchDataLoader to |callback_|, the other endpoint
// for which is passed to the browser process via
// blink.mojom.ServiceWorkerFetchResponseCallback.OnResponseStream().
class FetchLoaderClient final : public GarbageCollected<FetchLoaderClient>,
public FetchDataLoader::Client {
public:
FetchLoaderClient(
std::unique_ptr<ServiceWorkerEventQueue::StayAwakeToken> token)
: token_(std::move(token)) {
// We need to make |callback_| callable in the first place because some
// DidFetchDataLoadXXX() accessing it may be called synchronously from
// StartLoading().
callback_receiver_ = callback_.BindNewPipeAndPassReceiver();
}
void DidFetchDataStartedDataPipe(
mojo::ScopedDataPipeConsumerHandle pipe) override {
DCHECK(!body_stream_.is_valid());
DCHECK(pipe.is_valid());
body_stream_ = std::move(pipe);
}
void DidFetchDataLoadedDataPipe() override {
callback_->OnCompleted();
token_.reset();
}
void DidFetchDataLoadFailed() override {
callback_->OnAborted();
token_.reset();
}
void Abort() override {
// A fetch() aborted via AbortSignal in the ServiceWorker will just look
// like an ordinary failure to the page.
// TODO(ricea): Should a fetch() on the page get an AbortError instead?
callback_->OnAborted();
token_.reset();
}
mojom::blink::ServiceWorkerStreamHandlePtr CreateStreamHandle() {
if (!body_stream_.is_valid())
return nullptr;
return mojom::blink::ServiceWorkerStreamHandle::New(
std::move(body_stream_), std::move(callback_receiver_));
}
void Trace(Visitor* visitor) const override {
FetchDataLoader::Client::Trace(visitor);
}
private:
mojo::ScopedDataPipeConsumerHandle body_stream_;
mojo::PendingReceiver<mojom::blink::ServiceWorkerStreamCallback>
callback_receiver_;
mojo::Remote<mojom::blink::ServiceWorkerStreamCallback> callback_;
std::unique_ptr<ServiceWorkerEventQueue::StayAwakeToken> token_;
DISALLOW_COPY_AND_ASSIGN(FetchLoaderClient);
};
} // namespace
// This function may be called when an exception is scheduled. Thus, it must
// never invoke any code that might throw. In particular, it must never invoke
// JavaScript.
void FetchRespondWithObserver::OnResponseRejected(
ServiceWorkerResponseError error) {
DCHECK(GetExecutionContext());
const String error_message = GetMessageForResponseError(error, request_url_);
GetExecutionContext()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning, error_message));
// The default value of FetchAPIResponse's status is 0, which maps to a
// network error.
auto response = mojom::blink::FetchAPIResponse::New();
response->status_text = "";
response->error = error;
ServiceWorkerGlobalScope* service_worker_global_scope =
To<ServiceWorkerGlobalScope>(GetExecutionContext());
service_worker_global_scope->RespondToFetchEvent(
event_id_, request_url_, std::move(response), event_dispatch_time_,
base::TimeTicks::Now());
event_->RejectHandledPromise(error_message);
}
void FetchRespondWithObserver::OnResponseFulfilled(
ScriptState* script_state,
const ScriptValue& value,
ExceptionState::ContextType context_type,
const char* interface_name,
const char* property_name) {
DCHECK(GetExecutionContext());
if (!V8Response::HasInstance(value.V8Value(), script_state->GetIsolate())) {
OnResponseRejected(ServiceWorkerResponseError::kNoV8Instance);
return;
}
Response* response = V8Response::ToImplWithTypeCheck(
script_state->GetIsolate(), value.V8Value());
// "If one of the following conditions is true, return a network error:
// - |response|'s type is |error|.
// - |request|'s mode is |same-origin| and |response|'s type is |cors|.
// - |request|'s mode is not |no-cors| and response's type is |opaque|.
// - |request| is a client request and |response|'s type is neither
// |basic| nor |default|."
const network::mojom::FetchResponseType response_type =
response->GetResponse()->GetType();
if (response_type == network::mojom::FetchResponseType::kError) {
OnResponseRejected(ServiceWorkerResponseError::kResponseTypeError);
return;
}
if (response_type == network::mojom::FetchResponseType::kCors &&
request_mode_ == network::mojom::RequestMode::kSameOrigin) {
OnResponseRejected(
ServiceWorkerResponseError::kResponseTypeCorsForRequestModeSameOrigin);
return;
}
if (response_type == network::mojom::FetchResponseType::kOpaque) {
if (request_mode_ != network::mojom::RequestMode::kNoCors) {
OnResponseRejected(ServiceWorkerResponseError::kResponseTypeOpaque);
return;
}
// The request mode of client requests should be "same-origin" but it is
// not explicitly stated in the spec yet. So we need to check here.
// FIXME: Set the request mode of client requests to "same-origin" and
// remove this check when the spec will be updated.
// Spec issue: https://github.com/whatwg/fetch/issues/101
if (IsClientRequest(frame_type_, request_destination_)) {
OnResponseRejected(
ServiceWorkerResponseError::kResponseTypeOpaqueForClientRequest);
return;
}
}
if (redirect_mode_ != network::mojom::RedirectMode::kManual &&
response_type == network::mojom::FetchResponseType::kOpaqueRedirect) {
OnResponseRejected(ServiceWorkerResponseError::kResponseTypeOpaqueRedirect);
return;
}
if (redirect_mode_ != network::mojom::RedirectMode::kFollow &&
response->redirected()) {
OnResponseRejected(
ServiceWorkerResponseError::kRedirectedResponseForNotFollowRequest);
return;
}
if (response->IsBodyLocked()) {
OnResponseRejected(ServiceWorkerResponseError::kBodyLocked);
return;
}
if (response->IsBodyUsed()) {
OnResponseRejected(ServiceWorkerResponseError::kBodyUsed);
return;
}
mojom::blink::FetchAPIResponsePtr fetch_api_response =
response->PopulateFetchAPIResponse(request_url_);
ServiceWorkerGlobalScope* service_worker_global_scope =
To<ServiceWorkerGlobalScope>(GetExecutionContext());
// If Cross-Origin-Embedder-Policy is set to require-corp,
// Cross-Origin-Resource-Policy verification should happen before passing the
// response to the client. The service worker script must be in the same
// origin with the requestor, which is a client of the service worker.
//
// Here is in the renderer and we don't have a "trustworthy" initiator.
// Hence we provide |initiator_origin| as |request_initiator_origin_lock|.
auto initiator_origin =
url::Origin::Create(GURL(service_worker_global_scope->Url()));
// |corp_checker_| could be nullptr when the request is for a main resource
// or the connection to the client which initiated the request is broken.
// CORP check isn't needed in both cases because a service worker should be
// in the same origin with the main resource, and the response to the broken
// connection won't reach to the client.
if (corp_checker_ &&
corp_checker_->IsBlocked(
url::Origin::Create(GURL(service_worker_global_scope->Url())),
request_mode_, request_destination_, *response)) {
OnResponseRejected(ServiceWorkerResponseError::kDisallowedByCorp);
return;
}
BodyStreamBuffer* buffer = response->InternalBodyBuffer();
if (buffer) {
// The |side_data_blob| must be taken before the body buffer is
// drained or loading begins.
fetch_api_response->side_data_blob = buffer->TakeSideDataBlob();
ExceptionState exception_state(script_state->GetIsolate(), context_type,
interface_name, property_name);
scoped_refptr<BlobDataHandle> blob_data_handle =
buffer->DrainAsBlobDataHandle(
BytesConsumer::BlobSizePolicy::kAllowBlobWithInvalidSize);
if (blob_data_handle) {
// Handle the blob response body.
fetch_api_response->blob = blob_data_handle;
service_worker_global_scope->RespondToFetchEvent(
event_id_, request_url_, std::move(fetch_api_response),
event_dispatch_time_, base::TimeTicks::Now());
event_->ResolveHandledPromise();
return;
}
// Load the Response as a Mojo DataPipe. The resulting pipe consumer
// handle will be passed to the FetchLoaderClient on start.
FetchLoaderClient* fetch_loader_client =
MakeGarbageCollected<FetchLoaderClient>(
service_worker_global_scope->CreateStayAwakeToken());
buffer->StartLoading(FetchDataLoader::CreateLoaderAsDataPipe(task_runner_),
fetch_loader_client, exception_state);
if (exception_state.HadException()) {
OnResponseRejected(ServiceWorkerResponseError::kResponseBodyBroken);
return;
}
mojom::blink::ServiceWorkerStreamHandlePtr stream_handle =
fetch_loader_client->CreateStreamHandle();
// We failed to allocate the Mojo DataPipe.
if (!stream_handle) {
OnResponseRejected(ServiceWorkerResponseError::kDataPipeCreationFailed);
return;
}
service_worker_global_scope->RespondToFetchEventWithResponseStream(
event_id_, request_url_, std::move(fetch_api_response),
std::move(stream_handle), event_dispatch_time_, base::TimeTicks::Now());
event_->ResolveHandledPromise();
return;
}
service_worker_global_scope->RespondToFetchEvent(
event_id_, request_url_, std::move(fetch_api_response),
event_dispatch_time_, base::TimeTicks::Now());
event_->ResolveHandledPromise();
}
void FetchRespondWithObserver::OnNoResponse() {
DCHECK(GetExecutionContext());
if (request_body_stream_ && (request_body_stream_->IsLocked() ||
request_body_stream_->IsDisturbed())) {
GetExecutionContext()->CountUse(
WebFeature::kFetchRespondWithNoResponseWithUsedRequestBody);
if (!request_body_has_source_) {
OnResponseRejected(
mojom::blink::ServiceWorkerResponseError::kRequestBodyUnusable);
return;
}
}
if (request_body_stream_ && !request_body_has_source_) {
// TODO(crbug.com/1165690): Cancel `request_body_stream_`.
}
ServiceWorkerGlobalScope* service_worker_global_scope =
To<ServiceWorkerGlobalScope>(GetExecutionContext());
service_worker_global_scope->RespondToFetchEventWithNoResponse(
event_id_, request_url_, event_dispatch_time_, base::TimeTicks::Now());
event_->ResolveHandledPromise();
}
void FetchRespondWithObserver::SetEvent(FetchEvent* event) {
DCHECK(!event_);
DCHECK(!request_body_stream_);
event_ = event;
// We don't use Body::body() in order to avoid accidental CountUse calls.
BodyStreamBuffer* body_buffer = event_->request()->BodyBuffer();
if (body_buffer) {
request_body_stream_ = body_buffer->Stream();
}
}
FetchRespondWithObserver::FetchRespondWithObserver(
ExecutionContext* context,
int fetch_event_id,
base::WeakPtr<CrossOriginResourcePolicyChecker> corp_checker,
const mojom::blink::FetchAPIRequest& request,
WaitUntilObserver* observer)
: RespondWithObserver(context, fetch_event_id, observer),
request_url_(request.url),
request_mode_(request.mode),
redirect_mode_(request.redirect_mode),
frame_type_(request.frame_type),
request_destination_(request.destination),
request_body_has_source_(request.body.FormBody()),
corp_checker_(std::move(corp_checker)),
task_runner_(context->GetTaskRunner(TaskType::kNetworking)) {}
void FetchRespondWithObserver::Trace(Visitor* visitor) const {
visitor->Trace(event_);
visitor->Trace(request_body_stream_);
RespondWithObserver::Trace(visitor);
}
} // namespace blink