| // 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 |