blob: 108791de2504de76aa6de67ab4a92ced6ad6560f [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/core/fetch/response.h"
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "base/optional.h"
#include "services/network/public/mojom/fetch_api.mojom-blink.h"
#include "third_party/blink/public/platform/modules/service_worker/web_service_worker_response.h"
#include "third_party/blink/renderer/bindings/core/v8/dictionary.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer_view.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_form_data.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_readable_stream.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_url_search_params.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fetch/blob_bytes_consumer.h"
#include "third_party/blink/renderer/core/fetch/body_stream_buffer.h"
#include "third_party/blink/renderer/core/fetch/form_data_bytes_consumer.h"
#include "third_party/blink/renderer/core/fetch/response_init.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/html/forms/form_data.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/core/url/url_search_params.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/loader/cors/cors.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_utils.h"
#include "third_party/blink/renderer/platform/network/encoded_form_data.h"
#include "third_party/blink/renderer/platform/network/http_header_map.h"
#include "third_party/blink/renderer/platform/network/http_names.h"
#include "third_party/blink/renderer/platform/network/network_utils.h"
namespace blink {
namespace {
template <typename CORSHeadersContainer>
FetchResponseData* FilterResponseData(
network::mojom::FetchResponseType response_type,
FetchResponseData* response,
CORSHeadersContainer& headers) {
switch (response_type) {
case network::mojom::FetchResponseType::kBasic:
return response->CreateBasicFilteredResponse();
break;
case network::mojom::FetchResponseType::kCORS: {
WebHTTPHeaderSet header_names;
for (const auto& header : headers)
header_names.insert(header.Ascii().data());
return response->CreateCORSFilteredResponse(header_names);
break;
}
case network::mojom::FetchResponseType::kOpaque:
return response->CreateOpaqueFilteredResponse();
break;
case network::mojom::FetchResponseType::kOpaqueRedirect:
return response->CreateOpaqueRedirectFilteredResponse();
break;
case network::mojom::FetchResponseType::kDefault:
return response;
break;
case network::mojom::FetchResponseType::kError:
DCHECK_EQ(response->GetType(), network::mojom::FetchResponseType::kError);
return response;
break;
}
return response;
}
FetchResponseData* CreateFetchResponseDataFromFetchAPIResponse(
ScriptState* script_state,
mojom::blink::FetchAPIResponse& fetch_api_response) {
FetchResponseData* response = nullptr;
if (fetch_api_response.status_code > 0)
response = FetchResponseData::Create();
else
response = FetchResponseData::CreateNetworkErrorResponse();
response->SetURLList(fetch_api_response.url_list);
response->SetStatus(fetch_api_response.status_code);
response->SetStatusMessage(WTF::AtomicString(fetch_api_response.status_text));
response->SetResponseTime(fetch_api_response.response_time);
response->SetCacheStorageCacheName(
fetch_api_response.cache_storage_cache_name);
for (const auto& header : fetch_api_response.headers)
response->HeaderList()->Append(header.key, header.value);
if (fetch_api_response.blob) {
response->ReplaceBodyStreamBuffer(new BodyStreamBuffer(
script_state,
new BlobBytesConsumer(ExecutionContext::From(script_state),
fetch_api_response.blob),
nullptr /* AbortSignal */));
}
// Filter the response according to |fetch_api_response|'s ResponseType.
response = FilterResponseData(fetch_api_response.response_type, response,
fetch_api_response.cors_exposed_header_names);
return response;
}
// Checks whether |status| is a null body status.
// Spec: https://fetch.spec.whatwg.org/#null-body-status
bool IsNullBodyStatus(unsigned short status) {
if (status == 101 || status == 204 || status == 205 || status == 304)
return true;
return false;
}
// Check whether |statusText| is a ByteString and
// matches the Reason-Phrase token production.
// RFC 2616: https://tools.ietf.org/html/rfc2616
// RFC 7230: https://tools.ietf.org/html/rfc7230
// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
bool IsValidReasonPhrase(const String& status_text) {
for (unsigned i = 0; i < status_text.length(); ++i) {
UChar c = status_text[i];
if (!(c == 0x09 // HTAB
|| (0x20 <= c && c <= 0x7E) // SP / VCHAR
|| (0x80 <= c && c <= 0xFF))) // obs-text
return false;
}
return true;
}
} // namespace
Response* Response::Create(ScriptState* script_state,
ExceptionState& exception_state) {
return Create(script_state, nullptr, String(), ResponseInit::Create(),
exception_state);
}
Response* Response::Create(ScriptState* script_state,
ScriptValue body_value,
const ResponseInit* init,
ExceptionState& exception_state) {
v8::Local<v8::Value> body = body_value.V8Value();
v8::Isolate* isolate = script_state->GetIsolate();
ExecutionContext* execution_context = ExecutionContext::From(script_state);
BodyStreamBuffer* body_buffer = nullptr;
String content_type;
if (body_value.IsUndefined() || body_value.IsNull()) {
// Note: The IDL processor cannot handle this situation. See
// https://crbug.com/335871.
} else if (V8Blob::hasInstance(body, isolate)) {
Blob* blob = V8Blob::ToImpl(body.As<v8::Object>());
body_buffer = new BodyStreamBuffer(
script_state,
new BlobBytesConsumer(execution_context, blob->GetBlobDataHandle()),
nullptr /* AbortSignal */);
content_type = blob->type();
} else if (body->IsArrayBuffer()) {
// Avoid calling into V8 from the following constructor parameters, which
// is potentially unsafe.
DOMArrayBuffer* array_buffer = V8ArrayBuffer::ToImpl(body.As<v8::Object>());
body_buffer = new BodyStreamBuffer(script_state,
new FormDataBytesConsumer(array_buffer),
nullptr /* AbortSignal */);
} else if (body->IsArrayBufferView()) {
// Avoid calling into V8 from the following constructor parameters, which
// is potentially unsafe.
DOMArrayBufferView* array_buffer_view =
V8ArrayBufferView::ToImpl(body.As<v8::Object>());
body_buffer = new BodyStreamBuffer(
script_state, new FormDataBytesConsumer(array_buffer_view),
nullptr /* AbortSignal */);
} else if (V8FormData::hasInstance(body, isolate)) {
scoped_refptr<EncodedFormData> form_data =
V8FormData::ToImpl(body.As<v8::Object>())->EncodeMultiPartFormData();
// Here we handle formData->boundary() as a C-style string. See
// FormDataEncoder::generateUniqueBoundaryString.
content_type = AtomicString("multipart/form-data; boundary=") +
form_data->Boundary().data();
body_buffer = new BodyStreamBuffer(
script_state,
new FormDataBytesConsumer(execution_context, std::move(form_data)),
nullptr /* AbortSignal */);
} else if (V8URLSearchParams::hasInstance(body, isolate)) {
scoped_refptr<EncodedFormData> form_data =
V8URLSearchParams::ToImpl(body.As<v8::Object>())->ToEncodedFormData();
body_buffer = new BodyStreamBuffer(
script_state,
new FormDataBytesConsumer(execution_context, std::move(form_data)),
nullptr /* AbortSignal */);
content_type = "application/x-www-form-urlencoded;charset=UTF-8";
} else if (V8ReadableStream::hasInstance(body, isolate)) {
UseCounter::Count(execution_context,
WebFeature::kFetchResponseConstructionWithStream);
body_buffer = new BodyStreamBuffer(
script_state, V8ReadableStream::ToImpl(body.As<v8::Object>()));
} else {
String string = NativeValueTraits<IDLUSVString>::NativeValue(
isolate, body, exception_state);
if (exception_state.HadException())
return nullptr;
body_buffer =
new BodyStreamBuffer(script_state, new FormDataBytesConsumer(string),
nullptr /* AbortSignal */);
content_type = "text/plain;charset=UTF-8";
}
return Create(script_state, body_buffer, content_type, init, exception_state);
}
Response* Response::Create(ScriptState* script_state,
BodyStreamBuffer* body,
const String& content_type,
const ResponseInit* init,
ExceptionState& exception_state) {
unsigned short status = init->status();
// "1. If |init|'s status member is not in the range 200 to 599, inclusive,
// throw a RangeError."
if (status < 200 || 599 < status) {
exception_state.ThrowRangeError(
ExceptionMessages::IndexOutsideRange<unsigned>(
"status", status, 200, ExceptionMessages::kInclusiveBound, 599,
ExceptionMessages::kInclusiveBound));
return nullptr;
}
// "2. If |init|'s statusText member does not match the Reason-Phrase
// token production, throw a TypeError."
if (!IsValidReasonPhrase(init->statusText())) {
exception_state.ThrowTypeError("Invalid statusText");
return nullptr;
}
// "3. Let |r| be a new Response object, associated with a new response.
// "4. Set |r|'s headers to a new Headers object whose list is
// |r|'s response's header list, and guard is "response" "
Response* r = new Response(ExecutionContext::From(script_state));
// "5. Set |r|'s response's status to |init|'s status member."
r->response_->SetStatus(init->status());
// "6. Set |r|'s response's status message to |init|'s statusText member."
r->response_->SetStatusMessage(AtomicString(init->statusText()));
// "7. If |init|'s headers exists, then fill |r|’s headers with
// |init|'s headers"
if (init->hasHeaders()) {
// "1. Empty |r|'s response's header list."
r->response_->HeaderList()->ClearList();
// "2. Fill |r|'s Headers object with |init|'s headers member. Rethrow
// any exceptions."
r->headers_->FillWith(init->headers(), exception_state);
if (exception_state.HadException())
return nullptr;
}
// "8. If body is non-null, then:"
if (body) {
// "1. If |init|'s status is a null body status, then throw a TypeError."
if (IsNullBodyStatus(status)) {
exception_state.ThrowTypeError(
"Response with null body status cannot have body");
return nullptr;
}
// "2. Let |Content-Type| be null."
// "3. Set |r|'s response's body and |Content-Type|
// to the result of extracting body."
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
// Step 5, Blob:
// "If object's type attribute is not the empty byte sequence, set
// Content-Type to its value."
r->response_->ReplaceBodyStreamBuffer(body);
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
// Step 5, ReadableStream:
// "If object is disturbed or locked, then throw a TypeError."
// If the BodyStreamBuffer was not constructed from a ReadableStream
// then IsStreamLocked and IsStreamDisturbed will always be false.
// So we don't have to check BodyStreamBuffer is a ReadableStream
// or not.
if (body->IsStreamLocked(exception_state).value_or(true) ||
body->IsStreamDisturbed(exception_state).value_or(true)) {
if (!exception_state.HadException()) {
exception_state.ThrowTypeError(
"Response body object should not be disturbed or locked");
}
return nullptr;
}
// "4. If |Content-Type| is non-null and |r|'s response's header list
// contains no header named `Content-Type`, append `Content-Type`/
// |Content-Type| to |r|'s response's header list."
if (!content_type.IsEmpty() &&
!r->response_->HeaderList()->Has("Content-Type"))
r->response_->HeaderList()->Append("Content-Type", content_type);
}
// "9. Set |r|'s MIME type to the result of extracting a MIME type
// from |r|'s response's header list."
r->response_->SetMIMEType(r->response_->HeaderList()->ExtractMIMEType());
// "10. Set |r|'s response’s HTTPS state to current settings object's"
// HTTPS state."
// "11. Resolve |r|'s trailer promise with a new Headers object whose
// guard is "immutable"."
// "12. Return |r|."
return r;
}
Response* Response::Create(ExecutionContext* context,
FetchResponseData* response) {
return new Response(context, response);
}
Response* Response::Create(ScriptState* script_state,
mojom::blink::FetchAPIResponse& response) {
auto* fetch_response_data =
CreateFetchResponseDataFromFetchAPIResponse(script_state, response);
return new Response(ExecutionContext::From(script_state),
fetch_response_data);
}
Response* Response::error(ScriptState* script_state) {
FetchResponseData* response_data =
FetchResponseData::CreateNetworkErrorResponse();
Response* r =
new Response(ExecutionContext::From(script_state), response_data);
r->headers_->SetGuard(Headers::kImmutableGuard);
return r;
}
Response* Response::redirect(ScriptState* script_state,
const String& url,
unsigned short status,
ExceptionState& exception_state) {
KURL parsed_url = ExecutionContext::From(script_state)->CompleteURL(url);
if (!parsed_url.IsValid()) {
exception_state.ThrowTypeError("Failed to parse URL from " + url);
return nullptr;
}
if (!network_utils::IsRedirectResponseCode(status)) {
exception_state.ThrowRangeError("Invalid status code");
return nullptr;
}
Response* r = new Response(ExecutionContext::From(script_state));
r->headers_->SetGuard(Headers::kImmutableGuard);
r->response_->SetStatus(status);
r->response_->HeaderList()->Set("Location", parsed_url);
return r;
}
String Response::type() const {
// "The type attribute's getter must return response's type."
switch (response_->GetType()) {
case network::mojom::FetchResponseType::kBasic:
return "basic";
case network::mojom::FetchResponseType::kCORS:
return "cors";
case network::mojom::FetchResponseType::kDefault:
return "default";
case network::mojom::FetchResponseType::kError:
return "error";
case network::mojom::FetchResponseType::kOpaque:
return "opaque";
case network::mojom::FetchResponseType::kOpaqueRedirect:
return "opaqueredirect";
}
NOTREACHED();
return "";
}
String Response::url() const {
// "The url attribute's getter must return the empty string if response's
// url is null and response's url, serialized with the exclude fragment
// flag set, otherwise."
const KURL* response_url = response_->Url();
if (!response_url)
return g_empty_string;
if (!response_url->HasFragmentIdentifier())
return *response_url;
KURL url(*response_url);
url.RemoveFragmentIdentifier();
return url;
}
bool Response::redirected() const {
return response_->UrlList().size() > 1;
}
unsigned short Response::status() const {
// "The status attribute's getter must return response's status."
return response_->Status();
}
bool Response::ok() const {
// "The ok attribute's getter must return true
// if response's status is in the range 200 to 299, and false otherwise."
return cors::IsOkStatus(status());
}
String Response::statusText() const {
// "The statusText attribute's getter must return response's status message."
return response_->StatusMessage();
}
Headers* Response::headers() const {
// "The headers attribute's getter must return the associated Headers object."
return headers_;
}
Response* Response::clone(ScriptState* script_state,
ExceptionState& exception_state) {
if (IsBodyLocked(exception_state) == BodyLocked::kLocked ||
IsBodyUsed(exception_state) == BodyUsed::kUsed) {
DCHECK(!exception_state.HadException());
exception_state.ThrowTypeError("Response body is already used");
return nullptr;
}
if (exception_state.HadException())
return nullptr;
FetchResponseData* response = response_->Clone(script_state, exception_state);
if (exception_state.HadException())
return nullptr;
Headers* headers = Headers::Create(response->HeaderList());
headers->SetGuard(headers_->GetGuard());
return new Response(GetExecutionContext(), response, headers);
}
bool Response::HasPendingActivity() const {
if (!GetExecutionContext() || GetExecutionContext()->IsContextDestroyed())
return false;
if (!InternalBodyBuffer())
return false;
if (InternalBodyBuffer()->HasPendingActivity())
return true;
return Body::HasPendingActivity();
}
void Response::PopulateWebServiceWorkerResponse(
WebServiceWorkerResponse& response) {
response_->PopulateWebServiceWorkerResponse(response);
}
mojom::blink::FetchAPIResponsePtr Response::PopulateFetchAPIResponse() {
return response_->PopulateFetchAPIResponse();
}
Response::Response(ExecutionContext* context)
: Response(context, FetchResponseData::Create()) {}
Response::Response(ExecutionContext* context, FetchResponseData* response)
: Response(context, response, Headers::Create(response->HeaderList())) {
headers_->SetGuard(Headers::kResponseGuard);
}
Response::Response(ExecutionContext* context,
FetchResponseData* response,
Headers* headers)
: Body(context), response_(response), headers_(headers) {}
bool Response::HasBody() const {
return response_->InternalBuffer();
}
Body::BodyUsed Response::IsBodyUsed(ExceptionState& exception_state) {
auto* body_buffer = InternalBodyBuffer();
if (!body_buffer)
return BodyUsed::kUnused;
base::Optional<bool> stream_disturbed =
body_buffer->IsStreamDisturbed(exception_state);
if (exception_state.HadException())
return BodyUsed::kBroken;
DCHECK(stream_disturbed.has_value());
return stream_disturbed.value() ? BodyUsed::kUsed : BodyUsed::kUnused;
}
String Response::MimeType() const {
return response_->MimeType();
}
String Response::ContentType() const {
String result;
response_->HeaderList()->Get(http_names::kContentType, result);
return result;
}
String Response::InternalMIMEType() const {
return response_->InternalMIMEType();
}
const Vector<KURL>& Response::InternalURLList() const {
return response_->InternalURLList();
}
void Response::Trace(blink::Visitor* visitor) {
Body::Trace(visitor);
visitor->Trace(response_);
visitor->Trace(headers_);
}
bool Response::IsBodyUsedForDCheck(ExceptionState& exception_state) {
return InternalBodyBuffer() &&
InternalBodyBuffer()->IsStreamDisturbedForDCheck(exception_state);
}
} // namespace blink