blob: 27408e216aeb18d5b06732d5d0cce6913ccd2ea2 [file] [log] [blame]
// Copyright 2015 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/xhr/xml_http_request.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/compiler_specific.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/base/source_location.h"
#include "cobalt/base/tokens.h"
#include "cobalt/dom/csp_delegate.h"
#include "cobalt/dom/dom_settings.h"
#include "cobalt/dom/global_stats.h"
#include "cobalt/dom/progress_event.h"
#include "cobalt/dom/window.h"
#include "cobalt/dom/xml_document.h"
#include "cobalt/dom_parser/xml_decoder.h"
#include "cobalt/loader/cors_preflight.h"
#include "cobalt/loader/fetcher_factory.h"
#include "cobalt/loader/url_fetcher_string_writer.h"
#include "cobalt/script/global_environment.h"
#include "cobalt/script/javascript_engine.h"
#include "cobalt/xhr/xhr_modify_headers.h"
#include "nb/memory_scope.h"
#include "net/http/http_util.h"
namespace cobalt {
namespace xhr {
using dom::DOMException;
namespace {
// How many milliseconds must elapse between each progress event notification.
const int kProgressPeriodMs = 50;
const char* kResponseTypes[] = {
"", // kDefault
"text", // kText
"json", // kJson
"document", // kDocument
"blob", // kBlob
"arraybuffer", // kArrayBuffer
};
const char* kForbiddenMethods[] = {
"connect", "trace", "track",
};
bool MethodNameToRequestType(const std::string& method,
net::URLFetcher::RequestType* request_type) {
if (base::LowerCaseEqualsASCII(method, "get")) {
*request_type = net::URLFetcher::GET;
} else if (base::LowerCaseEqualsASCII(method, "post")) {
*request_type = net::URLFetcher::POST;
} else if (base::LowerCaseEqualsASCII(method, "head")) {
*request_type = net::URLFetcher::HEAD;
} else if (base::LowerCaseEqualsASCII(method, "delete")) {
*request_type = net::URLFetcher::DELETE_REQUEST;
} else if (base::LowerCaseEqualsASCII(method, "put")) {
*request_type = net::URLFetcher::PUT;
} else {
return false;
}
return true;
}
#if !defined(COBALT_BUILD_TYPE_GOLD)
const char* kStateNames[] = {"Unsent", "Opened", "HeadersReceived", "Loading",
"Done"};
const char* kMethodNames[] = {"GET", "POST", "HEAD", "DELETE", "PUT"};
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"
#endif
const char* RequestTypeToMethodName(net::URLFetcher::RequestType request_type) {
if (request_type >= 0 && request_type < arraysize(kMethodNames)) {
return kMethodNames[request_type];
} else {
NOTREACHED();
return "";
}
}
const char* StateName(XMLHttpRequest::State state) {
if (state >= 0 && state < arraysize(kStateNames)) {
return kStateNames[state];
} else {
NOTREACHED();
return "";
}
}
#if __clang__
#pragma clang diagnostic push
#endif
#endif // defined(COBALT_BUILD_TYPE_GOLD)
bool IsForbiddenMethod(const std::string& method) {
for (size_t i = 0; i < arraysize(kForbiddenMethods); ++i) {
if (base::LowerCaseEqualsASCII(method, kForbiddenMethods[i])) {
return true;
}
}
return false;
}
base::Token RequestErrorTypeName(XMLHttpRequest::RequestErrorType type) {
switch (type) {
case XMLHttpRequest::kNetworkError:
return base::Tokens::error();
case XMLHttpRequest::kTimeoutError:
return base::Tokens::timeout();
case XMLHttpRequest::kAbortError:
return base::Tokens::abort();
}
NOTREACHED();
return base::Token();
}
void FireProgressEvent(XMLHttpRequestEventTarget* target,
base::Token event_name) {
if (!target) {
return;
}
target->DispatchEvent(new dom::ProgressEvent(event_name));
}
void FireProgressEvent(XMLHttpRequestEventTarget* target,
base::Token event_name, uint64 loaded, uint64 total,
bool length_computable) {
if (!target) {
return;
}
target->DispatchEvent(
new dom::ProgressEvent(event_name, loaded, total, length_computable));
}
int s_xhr_sequence_num_ = 0;
// https://fetch.spec.whatwg.org/#concept-http-redirect-fetch
// 5. If request's redirect count is twenty, return a network error.
const int kRedirectLimit = 20;
} // namespace
bool XMLHttpRequest::verbose_ = false;
XMLHttpRequest::XMLHttpRequest(script::EnvironmentSettings* settings)
: XMLHttpRequestEventTarget(settings),
response_body_(new URLFetcherResponseWriter::Buffer(
URLFetcherResponseWriter::Buffer::kString)),
settings_(base::polymorphic_downcast<dom::DOMSettings*>(settings)),
state_(kUnsent),
response_type_(kDefault),
timeout_ms_(0),
method_(net::URLFetcher::GET),
http_status_(0),
with_credentials_(false),
error_(false),
sent_(false),
stop_timeout_(false),
upload_complete_(false),
active_requests_count_(0),
upload_listener_(false),
is_cross_origin_(false),
is_redirect_(false),
redirect_times_(0),
is_data_url_(false) {
DCHECK(settings_);
dom::GlobalStats::GetInstance()->Add(this);
xhr_id_ = ++s_xhr_sequence_num_;
}
void XMLHttpRequest::Abort() {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-abort()-method
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Cancel any in-flight request and set error flag.
TerminateRequest();
bool abort_is_no_op =
state_ == kUnsent || state_ == kDone || (state_ == kOpened && !sent_);
if (!abort_is_no_op) {
sent_ = false;
HandleRequestError(kAbortError);
}
ChangeState(kUnsent);
response_body_->Clear();
response_array_buffer_reference_.reset();
}
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-open()-method
void XMLHttpRequest::Open(const std::string& method, const std::string& url,
bool async,
const base::Optional<std::string>& username,
const base::Optional<std::string>& password,
script::ExceptionState* exception_state) {
TRACK_MEMORY_SCOPE("XHR");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
State previous_state = state_;
// Cancel any outstanding request.
TerminateRequest();
state_ = kUnsent;
if (!async) {
DLOG(ERROR) << "synchronous XHR is not supported";
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
base_url_ = settings_->base_url();
if (IsForbiddenMethod(method)) {
DOMException::Raise(DOMException::kSecurityErr, exception_state);
return;
}
if (!MethodNameToRequestType(method, &method_)) {
DOMException::Raise(DOMException::kSyntaxErr, exception_state);
return;
}
request_url_ = base_url_.Resolve(url);
if (!request_url_.is_valid()) {
DOMException::Raise(DOMException::kSyntaxErr, exception_state);
return;
}
dom::CspDelegate* csp = csp_delegate();
if (csp && !csp->CanLoad(dom::CspDelegate::kXhr, request_url_, false)) {
DOMException::Raise(DOMException::kSecurityErr, exception_state);
return;
}
sent_ = false;
stop_timeout_ = false;
PrepareForNewRequest();
// Check previous state to avoid dispatching readyState event when calling
// open several times in a row.
if (previous_state != kOpened) {
ChangeState(kOpened);
} else {
state_ = kOpened;
}
}
void XMLHttpRequest::SetRequestHeader(const std::string& header,
const std::string& value,
script::ExceptionState* exception_state) {
TRACK_MEMORY_SCOPE("XHR");
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#dom-xmlhttprequest-setrequestheader
if (state_ != kOpened || sent_) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
if (!net::HttpUtil::IsValidHeaderName(header) ||
!net::HttpUtil::IsValidHeaderValue(value)) {
DLOG(WARNING) << "Rejecting invalid header " << header << " : " << value;
return;
}
if (!net::HttpUtil::IsSafeHeader(header)) {
DLOG(WARNING) << "Rejecting unsafe header " << header;
return;
}
// Write the header if it is not set.
// If it is, append it to the existing one.
std::string cur_value;
if (request_headers_.GetHeader(header, &cur_value)) {
cur_value += ", " + value;
request_headers_.SetHeader(header, cur_value);
} else {
request_headers_.SetHeader(header, value);
}
}
void XMLHttpRequest::OverrideMimeType(const std::string& override_mime,
script::ExceptionState* exception_state) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#dom-xmlhttprequest-overridemimetype
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (state_ == kLoading || state_ == kDone) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
// Try to parse the given override. If it fails, throw an exception.
// Otherwise, we'll replace the content-type header in the response headers
// once we have them.
std::string mime_type;
std::string charset;
bool had_charset = false;
net::HttpUtil::ParseContentType(override_mime, &mime_type, &charset,
&had_charset, NULL);
if (!mime_type.length()) {
DOMException::Raise(DOMException::kSyntaxErr, exception_state);
return;
}
mime_type_override_ = mime_type;
}
void XMLHttpRequest::Send(script::ExceptionState* exception_state) {
Send(base::nullopt, exception_state);
}
void XMLHttpRequest::Send(const base::Optional<RequestBodyType>& request_body,
script::ExceptionState* exception_state) {
TRACK_MEMORY_SCOPE("XHR");
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-send()-method
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Step 1
if (state_ != kOpened) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
// Step 2
if (sent_) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
// Step 3 - 7
error_ = false;
upload_complete_ = false;
#if defined(COBALT_ENABLE_XHR_HEADER_FILTERING)
CobaltXhrModifyHeader(request_url_, &request_headers_);
#endif
// Add request body, if appropriate.
if ((method_ == net::URLFetcher::POST || method_ == net::URLFetcher::PUT) &&
request_body) {
bool has_content_type =
request_headers_.HasHeader(net::HttpRequestHeaders::kContentType);
if (request_body->IsType<std::string>()) {
request_body_text_.assign(request_body->AsType<std::string>());
if (!has_content_type) {
// We're assuming that request_body is UTF-8 encoded.
request_headers_.SetHeader(net::HttpRequestHeaders::kContentType,
"text/plain;charset=UTF-8");
}
} else if (request_body
->IsType<script::Handle<script::ArrayBufferView> >()) {
script::Handle<script::ArrayBufferView> view =
request_body->AsType<script::Handle<script::ArrayBufferView> >();
if (view->ByteLength()) {
const char* start = reinterpret_cast<const char*>(view->RawData());
request_body_text_.assign(start + view->ByteOffset(),
view->ByteLength());
}
} else if (request_body->IsType<script::Handle<script::ArrayBuffer> >()) {
script::Handle<script::ArrayBuffer> array_buffer =
request_body->AsType<script::Handle<script::ArrayBuffer> >();
if (array_buffer->ByteLength()) {
const char* start = reinterpret_cast<const char*>(array_buffer->Data());
request_body_text_.assign(start, array_buffer->ByteLength());
}
}
} else {
upload_complete_ = true;
}
// Step 8
if (upload_) {
upload_listener_ = upload_->HasOneOrMoreAttributeEventListener();
}
origin_ = settings_->document_origin();
// Step 9
sent_ = true;
// Now that a send is happening, prevent this object
// from being collected until it's complete or aborted
// if no currently active request has called it before.
IncrementActiveRequests();
FireProgressEvent(this, base::Tokens::loadstart());
if (!upload_complete_) {
FireProgressEvent(upload_, base::Tokens::loadstart());
}
// The loadstart callback may abort or modify the XHR request in some way.
// 11.3. If state is not opened or the send() flag is unset, then return.
if (state_ == kOpened && sent_) {
StartRequest(request_body_text_);
// Start the timeout timer running, if applicable.
send_start_time_ = base::TimeTicks::Now();
if (timeout_ms_) {
StartTimer(base::TimeDelta());
}
// Timer for throttling progress events.
upload_last_progress_time_ = base::TimeTicks();
last_progress_time_ = base::TimeTicks();
}
}
void XMLHttpRequest::Fetch(const FetchUpdateCallbackArg& fetch_callback,
const FetchModeCallbackArg& fetch_mode_callback,
const base::Optional<RequestBodyType>& request_body,
script::ExceptionState* exception_state) {
fetch_callback_.reset(
new FetchUpdateCallbackArg::Reference(this, fetch_callback));
fetch_mode_callback_.reset(
new FetchModeCallbackArg::Reference(this, fetch_mode_callback));
Send(request_body, exception_state);
}
base::Optional<std::string> XMLHttpRequest::GetResponseHeader(
const std::string& header) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-getresponseheader()-method
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (state_ == kUnsent || state_ == kOpened || error_) {
return base::nullopt;
}
// Set-Cookie should be stripped from the response headers in OnDone().
if (base::LowerCaseEqualsASCII(header, "set-cookie") ||
base::LowerCaseEqualsASCII(header, "set-cookie2")) {
return base::nullopt;
}
bool found;
std::string value;
if (net::HttpUtil::IsNonCoalescingHeader(header)) {
// A non-coalescing header may contain commas in the value, e.g. Date:
found = http_response_headers_->EnumerateHeader(NULL, header, &value);
} else {
found = http_response_headers_->GetNormalizedHeader(header, &value);
}
return found ? base::make_optional(value) : base::nullopt;
}
std::string XMLHttpRequest::GetAllResponseHeaders() {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-getallresponseheaders()-method
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
std::string output;
if (state_ == kUnsent || state_ == kOpened || error_) {
return output;
}
size_t iter = 0;
std::string name;
std::string value;
while (http_response_headers_->EnumerateHeaderLines(&iter, &name, &value)) {
output += name;
output += ": ";
output += value;
output += "\r\n";
}
return output;
}
const std::string& XMLHttpRequest::response_text(
script::ExceptionState* exception_state) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsetext-attribute
if (response_type_ != kDefault && response_type_ != kText) {
dom::DOMException::Raise(dom::DOMException::kInvalidStateErr,
exception_state);
}
if (error_ || (state_ != kLoading && state_ != kDone)) {
return base::EmptyString();
}
// Note that the conversion from |response_body_| to std::string when |state_|
// isn't kDone isn't efficient for large responses. Fortunately this feature
// is rarely used.
if (state_ == kLoading) {
LOG(WARNING) << "Retrieving responseText while loading can be inefficient.";
return response_body_->GetTemporaryReferenceOfString();
}
return response_body_->GetReferenceOfStringAndSeal();
}
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsexml-attribute
scoped_refptr<dom::Document> XMLHttpRequest::response_xml(
script::ExceptionState* exception_state) {
// 1. If responseType is not the empty string or "document", throw an
// "InvalidStateError" exception.
if (response_type_ != kDefault && response_type_ != kDocument) {
dom::DOMException::Raise(dom::DOMException::kInvalidStateErr,
exception_state);
return NULL;
}
// 2. If the state is not DONE, return null.
if (state_ != kDone) {
return NULL;
}
// 3. If the error flag is set, return null.
if (error_) {
return NULL;
}
// 4. Return the document response entity body.
return GetDocumentResponseEntityBody();
}
base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequest::response(
script::ExceptionState* exception_state) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#response
switch (response_type_) {
case kDefault:
case kText:
return ResponseType(response_text(exception_state));
case kArrayBuffer: {
script::Handle<script::ArrayBuffer> maybe_array_buffer_response =
response_array_buffer();
if (maybe_array_buffer_response.IsEmpty()) {
return base::nullopt;
}
return ResponseType(maybe_array_buffer_response);
}
case kJson:
case kDocument:
case kBlob:
case kResponseTypeCodeMax:
NOTIMPLEMENTED() << "Unsupported response_type_ "
<< response_type(exception_state);
}
return base::nullopt;
}
int XMLHttpRequest::status() const {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-status-attribute
if (state_ == kUnsent || state_ == kOpened || error_) {
return 0;
} else {
return http_status_;
}
}
std::string XMLHttpRequest::status_text() {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-statustext-attribute
if (state_ == kUnsent || state_ == kOpened || error_) {
return std::string();
}
return http_response_headers_->GetStatusText();
}
void XMLHttpRequest::set_response_type(
const std::string& response_type, script::ExceptionState* exception_state) {
if (state_ == kLoading || state_ == kDone) {
dom::DOMException::Raise(dom::DOMException::kInvalidStateErr,
exception_state);
return;
}
for (size_t i = 0; i < arraysize(kResponseTypes); ++i) {
if (response_type == kResponseTypes[i]) {
DCHECK_LT(i, kResponseTypeCodeMax);
response_type_ = static_cast<ResponseTypeCode>(i);
return;
}
}
DLOG(WARNING) << "Unexpected response type " << response_type;
}
std::string XMLHttpRequest::response_type(
script::ExceptionState* unused) const {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsetype-attribute
DCHECK_LT(response_type_, arraysize(kResponseTypes));
return kResponseTypes[response_type_];
}
void XMLHttpRequest::set_timeout(uint32 timeout) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-timeout-attribute
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
timeout_ms_ = timeout;
if (timeout_ms_ == 0) {
stop_timeout_ = true;
timer_.Stop();
} else if (sent_) {
// Timeout was set while request was in flight. Timeout is relative to
// the start of the request.
StartTimer(base::TimeTicks::Now() - send_start_time_);
}
}
bool XMLHttpRequest::with_credentials(script::ExceptionState* unused) const {
return with_credentials_;
}
void XMLHttpRequest::set_with_credentials(
bool with_credentials, script::ExceptionState* exception_state) {
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-withcredentials-attribute
if ((state_ != kUnsent && state_ != kOpened) || sent_) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
with_credentials_ = with_credentials;
}
scoped_refptr<XMLHttpRequestUpload> XMLHttpRequest::upload() {
if (!upload_) {
upload_ = new XMLHttpRequestUpload(settings_);
}
return upload_;
}
void XMLHttpRequest::OnURLFetchResponseStarted(const net::URLFetcher* source) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
http_status_ = source->GetResponseCode();
// Don't handle a response without headers.
if (!source->GetResponseHeaders()) {
HandleRequestError(kNetworkError);
return;
}
// Copy the response headers from the fetcher. It's not safe for us to
// modify the existing ones as they may be in use on the network thread.
http_response_headers_ =
new net::HttpResponseHeaders(source->GetResponseHeaders()->raw_headers());
// Perform a CORS Check on response headers at their arrival and raise
// Network Error when the Check fails.
if (is_cross_origin_) {
if (!loader::CORSPreflight::CORSCheck(*http_response_headers_,
origin_.SerializedOrigin(),
with_credentials_)) {
HandleRequestError(kNetworkError);
return;
}
}
// Discard these as required by XHR spec.
http_response_headers_->RemoveHeader("Set-Cookie2");
http_response_headers_->RemoveHeader("Set-Cookie");
http_response_headers_->GetMimeType(&response_mime_type_);
if (mime_type_override_.length()) {
http_response_headers_->RemoveHeader("Content-Type");
http_response_headers_->AddHeader(std::string("Content-Type: ") +
mime_type_override_);
}
if (fetch_mode_callback_) {
fetch_mode_callback_->value().Run(is_cross_origin_);
}
// Further filter response headers as XHR's mode is cors
if (is_cross_origin_) {
size_t iter = 0;
std::string name, value;
std::vector<std::pair<std::string, std::string> > header_names_to_discard;
std::vector<std::string> expose_headers;
loader::CORSPreflight::GetServerAllowedHeaders(*http_response_headers_,
&expose_headers);
while (http_response_headers_->EnumerateHeaderLines(&iter, &name, &value)) {
if (!loader::CORSPreflight::IsSafeResponseHeader(name, expose_headers,
with_credentials_)) {
header_names_to_discard.push_back(std::make_pair(name, value));
}
}
for (const auto& header : header_names_to_discard) {
http_response_headers_->RemoveHeaderLine(header.first, header.second);
}
}
if (is_data_url_) {
size_t iter = 0;
std::string name, value;
std::vector<std::pair<std::string, std::string> > header_names_to_discard;
while (http_response_headers_->EnumerateHeaderLines(&iter, &name, &value)) {
if (name != net::HttpRequestHeaders::kContentType) {
header_names_to_discard.push_back(std::make_pair(name, value));
}
}
for (const auto& header : header_names_to_discard) {
http_response_headers_->RemoveHeaderLine(header.first, header.second);
}
}
ChangeState(kHeadersReceived);
UpdateProgress(0);
}
void XMLHttpRequest::OnURLFetchDownloadProgress(const net::URLFetcher* source,
int64_t current, int64_t total,
int64_t current_network_bytes) {
TRACK_MEMORY_SCOPE("XHR");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK_NE(state_, kDone);
if (response_body_->HasProgressSinceLastGetAndReset() == 0) {
return;
}
// Signal to JavaScript that new data is now available.
ChangeState(kLoading);
if (fetch_callback_) {
std::string downloaded_data;
response_body_->GetAndReset(&downloaded_data);
script::Handle<script::Uint8Array> data =
script::Uint8Array::New(settings_->global_environment(),
downloaded_data.data(), downloaded_data.size());
fetch_callback_->value().Run(data);
}
// Send a progress notification if at least 50ms have elapsed.
const base::TimeTicks now = base::TimeTicks::Now();
const base::TimeDelta elapsed(now - last_progress_time_);
if (elapsed > base::TimeDelta::FromMilliseconds(kProgressPeriodMs)) {
last_progress_time_ = now;
// TODO: Investigate if we have to fire progress event with 0 loaded bytes
// when used as Fetch API.
UpdateProgress(response_body_->GetAndResetDownloadProgress());
}
}
void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (source->GetResponseHeaders()) {
if (source->GetResponseHeaders()->IsRedirect(NULL)) {
// To do CORS Check and Send potential preflight, we used
// SetStopOnRedirect to terminate request on redirect and OnRedict
// function will deal with the early termination and send preflight if
// needed.
OnRedirect(*source->GetResponseHeaders());
return;
}
}
const net::URLRequestStatus& status = source->GetStatus();
if (status.is_success()) {
stop_timeout_ = true;
if (error_) {
// Ensure the fetch callbacks are reset when URL fetch is complete,
// regardless of error status.
fetch_callback_.reset();
fetch_mode_callback_.reset();
return;
}
// Ensure all fetched data is read and transfered to this XHR. This should
// only be done for successful and error-free fetches.
OnURLFetchDownloadProgress(source, 0, 0, 0);
// The request may have completed too quickly, before URLFetcher's upload
// progress timer had a chance to inform us upload is finished.
if (!upload_complete_ && upload_listener_) {
upload_complete_ = true;
FireProgressEvent(upload_, base::Tokens::progress());
FireProgressEvent(upload_, base::Tokens::load());
FireProgressEvent(upload_, base::Tokens::loadend());
}
ChangeState(kDone);
UpdateProgress(response_body_->GetAndResetDownloadProgress());
// Undo the ref we added in Send()
DecrementActiveRequests();
} else {
HandleRequestError(kNetworkError);
}
fetch_callback_.reset();
fetch_mode_callback_.reset();
}
// Reset some variables in case the XHR object is reused.
void XMLHttpRequest::PrepareForNewRequest() {
request_headers_.Clear();
// Below are variables used for CORS.
request_body_text_.clear();
is_cross_origin_ = false;
redirect_times_ = 0;
is_data_url_ = false;
upload_listener_ = false;
is_redirect_ = false;
}
void XMLHttpRequest::OnURLFetchUploadProgress(const net::URLFetcher* source,
int64 current_val,
int64 total_val) {
TRACK_MEMORY_SCOPE("XHR");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (upload_complete_) {
return;
}
uint64 current = static_cast<uint64>(current_val);
uint64 total = static_cast<uint64>(total_val);
if (current == total) {
upload_complete_ = true;
}
// Fire a progress event if either the upload just completed, or if enough
// time has elapsed since we sent the last one.
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-send step 11.4.
// To process request body for request, run these subsubsteps:
// 1.not roughly 50ms have passed since these subsubsteps were last invoked,
// terminate these subsubsteps.
// 2. If upload listener flag is set, then fire a progress event named
// progress on the XMLHttpRequestUpload object with request's body's
// transmiteted bytes and request's body's total bytes.
if (!upload_listener_) {
return;
}
const base::TimeTicks now = base::TimeTicks::Now();
const base::TimeDelta elapsed(now - upload_last_progress_time_);
if (upload_complete_ ||
(elapsed > base::TimeDelta::FromMilliseconds(kProgressPeriodMs))) {
FireProgressEvent(upload_, base::Tokens::progress(), current, total,
total != 0);
upload_last_progress_time_ = now;
}
// To process request end-of-body for request, run these subsubsteps:
// 2. if upload listener flag is unset, then terminate these subsubsteps.
if (upload_complete_) {
FireProgressEvent(upload_, base::Tokens::load(), current, total,
total != 0);
FireProgressEvent(upload_, base::Tokens::loadend(), current, total,
total != 0);
}
}
void XMLHttpRequest::OnRedirect(const net::HttpResponseHeaders& headers) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
GURL new_url = url_fetcher_->GetURL();
// Since we moved redirect from url_request to here, we also need to
// handle redirecting too many times.
if (redirect_times_ >= kRedirectLimit) {
DLOG(INFO) << "XHR's redirect times hit limit, aborting request.";
HandleRequestError(kNetworkError);
return;
}
// This function is designed to be called by url_fetcher_core::
// OnReceivedRedirect
// https://fetch.spec.whatwg.org/#concept-http-redirect-fetch
// 7. If request’s mode is "cors", request’s origin is not same origin
// with actualResponse’s location URL’s origin, and actualResponse’s
// location URL includes credentials, then return a network error.
// 8. If CORS flag is set and actualResponse’s location URL includes
// credentials, then return a network error.
if (new_url.has_username() || new_url.has_password()) {
if (loader::Origin(new_url) != loader::Origin(request_url_)) {
DLOG(INFO) << "XHR is redirected to cross-origin url with credentials, "
"aborting request for security reasons.";
HandleRequestError(kNetworkError);
return;
} else if (is_cross_origin_) {
DLOG(INFO) << "XHR is redirected with credentials and cors_flag set, "
"aborting request for security reasons.";
HandleRequestError(kNetworkError);
return;
}
}
if (!new_url.is_valid()) {
HandleRequestError(kNetworkError);
return;
}
// This is a redirect. Re-check the CSP.
if (!csp_delegate()->CanLoad(dom::CspDelegate::kXhr, new_url,
true /* is_redirect */)) {
HandleRequestError(kNetworkError);
return;
}
// CORS check for the received resposne
if (is_cross_origin_) {
if (!loader::CORSPreflight::CORSCheck(headers, origin_.SerializedOrigin(),
with_credentials_)) {
HandleRequestError(kNetworkError);
return;
}
}
is_redirect_ = true;
// If CORS flag is set and actualResponse’s location URL’s origin is not
// same origin with request’s current url’s origin, then set request’s
// origin to a unique opaque origin.
if (loader::Origin(new_url) != loader::Origin(request_url_)) {
if (is_cross_origin_) {
origin_ = loader::Origin();
} else {
origin_ = loader::Origin(request_url_);
is_cross_origin_ = true;
}
}
// Send out preflight if needed
int http_status_code = headers.response_code();
if ((http_status_code == 303) ||
((http_status_code == 301 || http_status_code == 302) &&
(method_ == net::URLFetcher::POST))) {
method_ = net::URLFetcher::GET;
request_body_text_.clear();
}
request_url_ = new_url;
redirect_times_++;
StartRequest(request_body_text_);
}
void XMLHttpRequest::TraceMembers(script::Tracer* tracer) {
XMLHttpRequestEventTarget::TraceMembers(tracer);
tracer->Trace(upload_);
}
XMLHttpRequest::~XMLHttpRequest() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
dom::GlobalStats::GetInstance()->Remove(this);
}
dom::CspDelegate* XMLHttpRequest::csp_delegate() const {
DCHECK(settings_);
if (settings_->window() && settings_->window()->document()) {
return settings_->window()->document()->csp_delegate();
} else {
return NULL;
}
}
void XMLHttpRequest::TerminateRequest() {
error_ = true;
corspreflight_.reset(NULL);
url_fetcher_.reset(NULL);
}
void XMLHttpRequest::HandleRequestError(
XMLHttpRequest::RequestErrorType request_error_type) {
// https://www.w3.org/TR/XMLHttpRequest/#timeout-error
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DLOG_IF(INFO, verbose())
<< __FUNCTION__ << " (" << RequestErrorTypeName(request_error_type)
<< ") " << *this << std::endl
<< script::StackTraceToString(
settings_->global_environment()->GetStackTrace(0 /*max_frames*/));
stop_timeout_ = true;
// Step 1
TerminateRequest();
// Steps 2-4
// Change state and fire readystatechange event.
ChangeState(kDone);
base::Token error_name = RequestErrorTypeName(request_error_type);
// Step 5
if (!upload_complete_) {
upload_complete_ = true;
FireProgressEvent(upload_, base::Tokens::progress());
FireProgressEvent(upload_, error_name);
FireProgressEvent(upload_, base::Tokens::loadend());
}
// Steps 6-8
FireProgressEvent(this, base::Tokens::progress());
FireProgressEvent(this, error_name);
FireProgressEvent(this, base::Tokens::loadend());
fetch_callback_.reset();
fetch_mode_callback_.reset();
DecrementActiveRequests();
}
void XMLHttpRequest::OnTimeout() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (!stop_timeout_) {
HandleRequestError(kTimeoutError);
}
}
void XMLHttpRequest::StartTimer(base::TimeDelta time_since_send) {
// Subtract any time that has already elapsed from the timeout.
// This is in case the user has set a timeout after send() was already in
// flight.
base::TimeDelta delay = std::max(
base::TimeDelta(),
base::TimeDelta::FromMilliseconds(timeout_ms_) - time_since_send);
// Queue the callback even if delay ends up being zero, to preserve the
// previous semantics.
timer_.Start(FROM_HERE, delay, this, &XMLHttpRequest::OnTimeout);
}
void XMLHttpRequest::ChangeState(XMLHttpRequest::State new_state) {
// Always dispatch state change events for LOADING, also known as
// INTERACTIVE, so that clients can get partial data (XHR streaming).
// This is to match the behavior of Chrome (which took it from Firefox).
if (state_ == new_state && new_state != kLoading) {
return;
}
state_ = new_state;
if (state_ != kUnsent) {
DispatchEvent(new dom::Event(base::Tokens::readystatechange()));
}
}
script::Handle<script::ArrayBuffer> XMLHttpRequest::response_array_buffer() {
TRACK_MEMORY_SCOPE("XHR");
// https://www.w3.org/TR/XMLHttpRequest/#response-entity-body
if (error_ || state_ != kDone) {
// Return a handle holding a nullptr.
return script::Handle<script::ArrayBuffer>();
}
if (!response_array_buffer_reference_) {
// The request is done so it is safe to only keep the ArrayBuffer and clear
// |response_body_|. As |response_body_| will not be used unless the
// request is re-opened.
std::unique_ptr<script::PreallocatedArrayBufferData> downloaded_data(
new script::PreallocatedArrayBufferData());
response_body_->GetAndReset(downloaded_data.get());
auto array_buffer = script::ArrayBuffer::New(
settings_->global_environment(), std::move(downloaded_data));
response_array_buffer_reference_.reset(
new script::ScriptValue<script::ArrayBuffer>::Reference(this,
array_buffer));
return array_buffer;
} else {
return script::Handle<script::ArrayBuffer>(
*response_array_buffer_reference_);
}
}
void XMLHttpRequest::UpdateProgress(int64_t received_length) {
DCHECK(http_response_headers_);
const int64 content_length = http_response_headers_->GetContentLength();
const bool length_computable =
content_length > 0 && received_length <= content_length;
const uint64 total =
length_computable ? static_cast<uint64>(content_length) : 0;
DLOG_IF(INFO, verbose()) << __FUNCTION__ << " (" << received_length << " / "
<< total << ") " << *this;
if (state_ == kDone) {
FireProgressEvent(this, base::Tokens::load(),
static_cast<uint64>(received_length), total,
length_computable);
FireProgressEvent(this, base::Tokens::loadend(),
static_cast<uint64>(received_length), total,
length_computable);
} else {
FireProgressEvent(this, base::Tokens::progress(),
static_cast<uint64>(received_length), total,
length_computable);
}
}
void XMLHttpRequest::IncrementActiveRequests() {
if (active_requests_count_ == 0) {
prevent_gc_until_send_complete_.reset(
new script::GlobalEnvironment::ScopedPreventGarbageCollection(
settings_->global_environment(), this));
}
active_requests_count_++;
}
void XMLHttpRequest::DecrementActiveRequests() {
DCHECK_GT(active_requests_count_, 0);
active_requests_count_--;
if (active_requests_count_ == 0) {
bool is_active = (state_ == kOpened && sent_) ||
state_ == kHeadersReceived || state_ == kLoading;
bool has_event_listeners =
GetAttributeEventListener(base::Tokens::readystatechange()) ||
GetAttributeEventListener(base::Tokens::progress()) ||
GetAttributeEventListener(base::Tokens::abort()) ||
GetAttributeEventListener(base::Tokens::error()) ||
GetAttributeEventListener(base::Tokens::load()) ||
GetAttributeEventListener(base::Tokens::timeout()) ||
GetAttributeEventListener(base::Tokens::loadend());
DCHECK_EQ((is_active && has_event_listeners), false);
prevent_gc_until_send_complete_.reset();
}
}
void XMLHttpRequest::StartRequest(const std::string& request_body) {
TRACK_MEMORY_SCOPE("XHR");
response_array_buffer_reference_.reset();
network::NetworkModule* network_module =
settings_->fetcher_factory()->network_module();
url_fetcher_ = net::URLFetcher::Create(request_url_, method_, this);
url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
if (fetch_callback_) {
response_body_ = new URLFetcherResponseWriter::Buffer(
URLFetcherResponseWriter::Buffer::kString);
response_body_->DisablePreallocate();
} else {
response_body_ = new URLFetcherResponseWriter::Buffer(
response_type_ == kArrayBuffer
? URLFetcherResponseWriter::Buffer::kArrayBuffer
: URLFetcherResponseWriter::Buffer::kString);
}
std::unique_ptr<net::URLFetcherResponseWriter> download_data_writer(
new URLFetcherResponseWriter(response_body_));
url_fetcher_->SaveResponseWithWriter(std::move(download_data_writer));
// Don't retry, let the caller deal with it.
url_fetcher_->SetAutomaticallyRetryOn5xx(false);
url_fetcher_->SetExtraRequestHeaders(request_headers_.ToString());
// We want to do cors check and preflight during redirects
url_fetcher_->SetStopOnRedirect(true);
if (request_body.size()) {
// If applicable, the request body Content-Type is already set in
// request_headers.
url_fetcher_->SetUploadData("", request_body);
}
// We let data url fetch resources freely but with no response headers.
is_data_url_ = is_data_url_ || request_url_.SchemeIs("data");
is_cross_origin_ = (is_redirect_ && is_cross_origin_) ||
(origin_ != loader::Origin(request_url_) && !is_data_url_);
is_redirect_ = false;
// If the CORS flag is set, httpRequest’s method is neither `GET` nor `HEAD`
// or httpRequest’s mode is "websocket", then append `Origin`/httpRequest’s
// origin, serialized and UTF-8 encoded, to httpRequest’s header list.
if (is_cross_origin_ ||
(method_ != net::URLFetcher::GET && method_ != net::URLFetcher::HEAD)) {
url_fetcher_->AddExtraRequestHeader("Origin:" + origin_.SerializedOrigin());
}
bool dopreflight = false;
if (is_cross_origin_) {
corspreflight_.reset(new cobalt::loader::CORSPreflight(
request_url_, method_, network_module,
base::Bind(&XMLHttpRequest::CORSPreflightSuccessCallback,
base::Unretained(this)),
origin_.SerializedOrigin(),
base::Bind(&XMLHttpRequest::CORSPreflightErrorCallback,
base::Unretained(this)),
settings_->window()->get_preflight_cache()));
corspreflight_->set_headers(request_headers_);
// For cross-origin requests, don't send or save auth data / cookies unless
// withCredentials was set.
// To make a cross-origin request, add origin, referrer source, credentials,
// omit credentials flag, force preflight flag
if (!with_credentials_) {
const uint32 kDisableCookiesLoadFlags =
net::LOAD_NORMAL | net::LOAD_DO_NOT_SAVE_COOKIES |
net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SEND_AUTH_DATA;
url_fetcher_->SetLoadFlags(kDisableCookiesLoadFlags);
} else {
// For credentials mode: If the withCredentials attribute value is true,
// "include", and "same-origin" otherwise.
corspreflight_->set_credentials_mode_is_include(true);
}
corspreflight_->set_force_preflight(upload_listener_);
dopreflight = corspreflight_->Send();
}
DLOG_IF(INFO, verbose()) << __FUNCTION__ << *this;
if (!dopreflight) {
url_fetcher_->Start();
}
}
void XMLHttpRequest::CORSPreflightErrorCallback() {
HandleRequestError(XMLHttpRequest::kNetworkError);
}
void XMLHttpRequest::CORSPreflightSuccessCallback() {
if (url_fetcher_) {
url_fetcher_->Start();
}
}
std::ostream& operator<<(std::ostream& out, const XMLHttpRequest& xhr) {
#if !defined(COBALT_BUILD_TYPE_GOLD)
base::StringPiece response_text("");
if ((xhr.state_ == XMLHttpRequest::kDone) &&
(xhr.response_type_ == XMLHttpRequest::kDefault ||
xhr.response_type_ == XMLHttpRequest::kText)) {
size_t kMaxSize = 4096;
const auto& response_body =
xhr.response_body_->GetTemporaryReferenceOfString();
response_text =
base::StringPiece(reinterpret_cast<const char*>(response_body.data()),
std::min(kMaxSize, response_body.size()));
}
std::string xhr_out = base::StringPrintf(
" XHR:\n"
"\tid: %d\n"
"\trequest_url: %s\n"
"\tstate: %s\n"
"\tresponse_type: %s\n"
"\ttimeout_ms: %d\n"
"\tmethod: %s\n"
"\thttp_status: %d\n"
"\twith_credentials: %s\n"
"\terror: %s\n"
"\tsent: %s\n"
"\tstop_timeout: %s\n"
"\tresponse_body: %s\n",
xhr.xhr_id_, xhr.request_url_.spec().c_str(), StateName(xhr.state_),
xhr.response_type(NULL).c_str(), xhr.timeout_ms_,
RequestTypeToMethodName(xhr.method_), xhr.http_status_,
xhr.with_credentials_ ? "true" : "false", xhr.error_ ? "true" : "false",
xhr.sent_ ? "true" : "false", xhr.stop_timeout_ ? "true" : "false",
response_text.as_string().c_str());
out << xhr_out;
#else
#endif
return out;
}
// https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#document-response-entity-body
scoped_refptr<dom::Document> XMLHttpRequest::GetDocumentResponseEntityBody() {
DCHECK_EQ(state_, kDone);
// Step 1..5
const std::string final_mime_type =
mime_type_override_.empty() ? response_mime_type_ : mime_type_override_;
if (final_mime_type != "text/xml" && final_mime_type != "application/xml") {
return NULL;
}
// 6. Otherwise, let document be a document that represents the result of
// parsing the response entity body following the rules set forth in the XML
// specifications. If that fails (unsupported character encoding, namespace
// well-formedness error, etc.), return null.
scoped_refptr<dom::XMLDocument> xml_document =
new dom::XMLDocument(settings_->window()->html_element_context());
dom_parser::XMLDecoder xml_decoder(
xml_document, xml_document, NULL, settings_->max_dom_element_depth(),
base::SourceLocation("[object XMLHttpRequest]", 1, 1),
base::Bind(&XMLHttpRequest::XMLDecoderLoadCompleteCallback,
base::Unretained(this)));
has_xml_decoder_error_ = false;
xml_decoder.DecodeChunk(response_body_->GetReferenceOfStringAndSeal().c_str(),
response_body_->GetReferenceOfStringAndSeal().size());
xml_decoder.Finish();
if (has_xml_decoder_error_) {
return NULL;
}
// Step 7..11 Not needed by Cobalt.
// 12. Return document.
return xml_document;
}
void XMLHttpRequest::XMLDecoderLoadCompleteCallback(
const base::Optional<std::string>& error) {
if (error) has_xml_decoder_error_ = true;
}
} // namespace xhr
} // namespace cobalt