| // 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/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/task_runner.h" |
| #include "base/threading/thread_task_runner_handle.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/performance.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/fetch_interceptor_coordinator.h" |
| #include "cobalt/loader/fetcher_factory.h" |
| #include "cobalt/loader/url_fetcher_string_writer.h" |
| #include "cobalt/network/network_module.h" |
| #include "cobalt/script/global_environment.h" |
| #include "cobalt/script/javascript_engine.h" |
| #include "cobalt/web/context.h" |
| #include "cobalt/web/csp_delegate.h" |
| #include "cobalt/web/environment_settings.h" |
| #include "cobalt/xhr/global_stats.h" |
| #include "nb/memory_scope.h" |
| #include "net/http/http_util.h" |
| |
| namespace cobalt { |
| namespace xhr { |
| |
| using web::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", |
| }; |
| |
| // https://www.w3.org/TR/resource-timing-1/#dom-performanceresourcetiming-initiatortype |
| const char* kPerformanceResourceTimingInitiatorType = "xmlhttprequest"; |
| |
| 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)); |
| } |
| |
| #if !defined(COBALT_BUILD_TYPE_GOLD) |
| int s_xhr_sequence_num_ = 0; |
| #endif // !defined(COBALT_BUILD_TYPE_GOLD) |
| // 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; |
| |
| std::string ClipUrl(const GURL& url, size_t length) { |
| const std::string& spec = url.possibly_invalid_spec(); |
| if (spec.size() < length) { |
| return spec; |
| } |
| |
| size_t remain = length - 5; |
| size_t head = remain / 2; |
| size_t tail = remain - head; |
| |
| return spec.substr(0, head) + "[...]" + spec.substr(spec.size() - tail); |
| } |
| } // namespace |
| |
| bool XMLHttpRequestImpl::verbose_ = false; |
| |
| XMLHttpRequest::XMLHttpRequest(script::EnvironmentSettings* settings) |
| : XMLHttpRequestEventTarget(settings) { |
| // Determine which implementation of XHR to use based on being in a window or |
| // worker. |
| web::WindowOrWorkerGlobalScope* window_or_worker_global_scope = |
| environment_settings()->context()->GetWindowOrWorkerGlobalScope(); |
| if (window_or_worker_global_scope->IsWindow()) { |
| xhr_impl_ = std::make_unique<DOMXMLHttpRequestImpl>(this); |
| } else if (window_or_worker_global_scope->IsDedicatedWorker() || |
| window_or_worker_global_scope->IsServiceWorker()) { |
| xhr_impl_ = std::make_unique<XMLHttpRequestImpl>(this); |
| } |
| xhr::GlobalStats::GetInstance()->Add(this); |
| #if !defined(COBALT_BUILD_TYPE_GOLD) |
| xhr_id_ = ++s_xhr_sequence_num_; |
| #endif // !defined(COBALT_BUILD_TYPE_GOLD) |
| } |
| |
| XMLHttpRequest::~XMLHttpRequest() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| xhr::GlobalStats::GetInstance()->Remove(this); |
| } |
| |
| void XMLHttpRequest::Abort() { xhr_impl_->Abort(); } |
| 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) { |
| xhr_impl_->Open(method, url, async, username, password, exception_state); |
| } |
| |
| // Must be called after open(), but before send(). |
| void XMLHttpRequest::SetRequestHeader(const std::string& header, |
| const std::string& value, |
| script::ExceptionState* exception_state) { |
| xhr_impl_->SetRequestHeader(header, value, exception_state); |
| } |
| |
| // Override the MIME type returned by the server. |
| // Call before Send(), otherwise throws InvalidStateError. |
| void XMLHttpRequest::OverrideMimeType(const std::string& mime_type, |
| script::ExceptionState* exception_state) { |
| xhr_impl_->OverrideMimeType(mime_type, exception_state); |
| } |
| |
| void XMLHttpRequest::Send(const base::Optional<RequestBodyType>& request_body, |
| script::ExceptionState* exception_state) { |
| xhr_impl_->Send(request_body, exception_state); |
| } |
| |
| void XMLHttpRequest::Fetch(const FetchUpdateCallbackArg& fetch_callback, |
| const FetchModeCallbackArg& fetch_mode_callback, |
| const base::Optional<RequestBodyType>& request_body, |
| script::ExceptionState* exception_state) { |
| xhr_impl_->Fetch(fetch_callback, fetch_mode_callback, request_body, |
| exception_state); |
| } |
| |
| base::Optional<std::string> XMLHttpRequest::GetResponseHeader( |
| const std::string& header) { |
| return xhr_impl_->GetResponseHeader(header); |
| } |
| std::string XMLHttpRequest::GetAllResponseHeaders() { |
| return xhr_impl_->GetAllResponseHeaders(); |
| } |
| |
| const std::string& XMLHttpRequest::response_text( |
| script::ExceptionState* exception_state) { |
| return xhr_impl_->response_text(exception_state); |
| } |
| scoped_refptr<dom::Document> XMLHttpRequest::response_xml( |
| script::ExceptionState* exception_state) { |
| return xhr_impl_->response_xml(exception_state); |
| } |
| base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequest::response( |
| script::ExceptionState* exception_state) { |
| return xhr_impl_->response(exception_state); |
| } |
| |
| int XMLHttpRequest::ready_state() const { return xhr_impl_->ready_state(); } |
| int XMLHttpRequest::status() const { return xhr_impl_->status(); } |
| std::string XMLHttpRequest::status_text() { return xhr_impl_->status_text(); } |
| void XMLHttpRequest::set_response_type( |
| const std::string& response_type, script::ExceptionState* exception_state) { |
| xhr_impl_->set_response_type(response_type, exception_state); |
| } |
| std::string XMLHttpRequest::response_type( |
| script::ExceptionState* exception_state) const { |
| return xhr_impl_->response_type(exception_state); |
| } |
| |
| uint32 XMLHttpRequest::timeout() const { return xhr_impl_->timeout(); } |
| void XMLHttpRequest::set_timeout(uint32 timeout) { |
| xhr_impl_->set_timeout(timeout); |
| } |
| bool XMLHttpRequest::with_credentials( |
| script::ExceptionState* exception_state) const { |
| return xhr_impl_->with_credentials(exception_state); |
| } |
| void XMLHttpRequest::set_with_credentials( |
| bool b, script::ExceptionState* exception_state) { |
| xhr_impl_->set_with_credentials(b, exception_state); |
| } |
| |
| scoped_refptr<XMLHttpRequestUpload> XMLHttpRequest::upload() { |
| return xhr_impl_->upload(); |
| } |
| |
| void XMLHttpRequest::set_verbose(bool verbose) { |
| XMLHttpRequestImpl::set_verbose(verbose); |
| } |
| bool XMLHttpRequest::verbose() { return XMLHttpRequestImpl::verbose(); } |
| |
| // net::URLFetcherDelegate interface |
| void XMLHttpRequest::OnURLFetchResponseStarted(const net::URLFetcher* source) { |
| xhr_impl_->OnURLFetchResponseStarted(source); |
| } |
| void XMLHttpRequest::OnURLFetchDownloadProgress(const net::URLFetcher* source, |
| int64_t current, int64_t total, |
| int64_t current_network_bytes) { |
| xhr_impl_->OnURLFetchDownloadProgress(source, current, total, |
| current_network_bytes, |
| /* request_done = */ false); |
| } |
| void XMLHttpRequest::OnURLFetchComplete(const net::URLFetcher* source) { |
| xhr_impl_->OnURLFetchComplete(source); |
| } |
| |
| void XMLHttpRequest::OnURLFetchUploadProgress(const net::URLFetcher* source, |
| int64 current, int64 total) { |
| xhr_impl_->OnURLFetchUploadProgress(source, current, total); |
| } |
| void XMLHttpRequest::OnRedirect(const net::HttpResponseHeaders& headers) { |
| xhr_impl_->OnRedirect(headers); |
| } |
| |
| // Called from bindings layer to tie objects' lifetimes to this XHR instance. |
| XMLHttpRequestUpload* XMLHttpRequest::upload_or_null() { |
| return xhr_impl_->upload_or_null(); |
| } |
| |
| void XMLHttpRequest::ReportLoadTimingInfo( |
| const net::LoadTimingInfo& timing_info) { |
| xhr_impl_->ReportLoadTimingInfo(timing_info); |
| } |
| // Create Performance Resource Timing entry for XMLHttpRequest. |
| void XMLHttpRequest::GetLoadTimingInfoAndCreateResourceTiming() { |
| xhr_impl_->GetLoadTimingInfoAndCreateResourceTiming(); |
| } |
| |
| void XMLHttpRequest::TraceMembers(script::Tracer* tracer) { |
| XMLHttpRequestEventTarget::TraceMembers(tracer); |
| xhr_impl_->TraceMembers(tracer); |
| } |
| |
| XMLHttpRequestImpl::XMLHttpRequestImpl(XMLHttpRequest* xhr) |
| : error_(false), |
| is_cross_origin_(false), |
| cors_policy_(xhr->environment_settings() |
| ->context() |
| ->fetcher_factory() |
| ->network_module() |
| ->network_delegate() |
| ->cors_policy()), |
| is_data_url_(false), |
| is_redirect_(false), |
| method_(net::URLFetcher::GET), |
| response_body_(new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kString, |
| xhr->environment_settings() |
| ->context() |
| ->web_settings() |
| ->xhr_settings() |
| .IsTryLockForProgressCheckEnabled())), |
| response_type_(XMLHttpRequest::kDefault), |
| state_(XMLHttpRequest::kUnsent), |
| upload_listener_(false), |
| with_credentials_(false), |
| xhr_(xhr), |
| will_destroy_current_message_loop_(false), |
| active_requests_count_(0), |
| http_status_(0), |
| redirect_times_(0), |
| sent_(false), |
| settings_(xhr->environment_settings()), |
| stop_timeout_(false), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| timeout_ms_(0), |
| upload_complete_(false) { |
| DCHECK(environment_settings()); |
| base::MessageLoop::current()->AddDestructionObserver(this); |
| } |
| |
| void XMLHttpRequestImpl::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_ == XMLHttpRequest::kUnsent || |
| state_ == XMLHttpRequest::kDone || |
| (state_ == XMLHttpRequest::kOpened && !sent_); |
| if (!abort_is_no_op) { |
| sent_ = false; |
| HandleRequestError(XMLHttpRequest::kAbortError); |
| } |
| ChangeState(XMLHttpRequest::kUnsent); |
| |
| response_body_->Clear(); |
| response_array_buffer_reference_.reset(); |
| } |
| |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-open()-method |
| void XMLHttpRequestImpl::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_); |
| |
| XMLHttpRequest::State previous_state = state_; |
| |
| // Cancel any outstanding request. |
| TerminateRequest(); |
| state_ = XMLHttpRequest::kUnsent; |
| |
| if (!async) { |
| DLOG(ERROR) << "synchronous XHR is not supported"; |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return; |
| } |
| |
| base_url_ = environment_settings()->base_url(); |
| |
| if (IsForbiddenMethod(method)) { |
| web::DOMException::Raise(web::DOMException::kSecurityErr, exception_state); |
| return; |
| } |
| |
| if (!MethodNameToRequestType(method, &method_)) { |
| web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state); |
| return; |
| } |
| |
| request_url_ = base_url_.Resolve(url); |
| if (!request_url_.is_valid()) { |
| web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state); |
| return; |
| } |
| |
| web::CspDelegate* csp = csp_delegate(); |
| if (csp && !csp->CanLoad(web::CspDelegate::kXhr, request_url_, false)) { |
| web::DOMException::Raise(web::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 != XMLHttpRequest::kOpened) { |
| ChangeState(XMLHttpRequest::kOpened); |
| } else { |
| state_ = XMLHttpRequest::kOpened; |
| } |
| } |
| |
| void XMLHttpRequestImpl::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_ != XMLHttpRequest::kOpened || sent_) { |
| web::DOMException::Raise(web::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 XMLHttpRequestImpl::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_ == XMLHttpRequest::kLoading || state_ == XMLHttpRequest::kDone) { |
| web::DOMException::Raise(web::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()) { |
| web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state); |
| return; |
| } |
| mime_type_override_ = mime_type; |
| } |
| |
| void XMLHttpRequestImpl::Send( |
| const base::Optional<XMLHttpRequest::RequestBodyType>& request_body, |
| script::ExceptionState* exception_state) { |
| error_ = false; |
| bool in_service_worker = environment_settings() |
| ->context() |
| ->GetWindowOrWorkerGlobalScope() |
| ->IsServiceWorker(); |
| if (!in_service_worker && method_ == net::URLFetcher::GET) { |
| loader::FetchInterceptorCoordinator::GetInstance()->TryIntercept( |
| request_url_, /*main_resource=*/false, request_headers_, task_runner_, |
| base::BindOnce(&XMLHttpRequestImpl::SendIntercepted, AsWeakPtr()), |
| base::BindOnce(&XMLHttpRequestImpl::ReportLoadTimingInfo, AsWeakPtr()), |
| base::BindOnce(&XMLHttpRequestImpl::SendFallback, AsWeakPtr(), |
| request_body, exception_state)); |
| return; |
| } |
| SendFallback(request_body, exception_state); |
| } |
| |
| void XMLHttpRequestImpl::SendIntercepted( |
| std::unique_ptr<std::string> response) { |
| if (will_destroy_current_message_loop_.load()) { |
| return; |
| } |
| if (task_runner_ != base::ThreadTaskRunnerHandle::Get()) { |
| task_runner_->PostTask(FROM_HERE, |
| base::BindOnce(&XMLHttpRequestImpl::SendIntercepted, |
| AsWeakPtr(), std::move(response))); |
| return; |
| } |
| 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. |
| // TODO: consider deduplicating code from |Send()|, |
| // |OnURLFetchDownloadProgress()|, and |OnURLFetchComplete()|. |
| |
| // Send(). |
| IncrementActiveRequests(); |
| FireProgressEvent(xhr_, base::Tokens::loadstart()); |
| if (!upload_complete_) { |
| FireProgressEvent(upload_, base::Tokens::loadstart()); |
| } |
| |
| // OnURLFetchResponseStarted(). |
| http_status_ = 200; |
| http_response_headers_ = new net::HttpResponseHeaders(""); |
| ChangeState(XMLHttpRequest::kHeadersReceived); |
| |
| // OnURLFetchDownloadProgress(). |
| ChangeState(XMLHttpRequest::kLoading); |
| const auto& xhr_settings = |
| environment_settings()->context()->web_settings()->xhr_settings(); |
| response_body_ = new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kString, |
| xhr_settings.IsTryLockForProgressCheckEnabled()); |
| response_body_->Write(response->data(), response->size()); |
| if (fetch_callback_) { |
| script::Handle<script::Uint8Array> data = |
| script::Uint8Array::New(settings_->context()->global_environment(), |
| response->data(), response->size()); |
| fetch_callback_->value().Run(data); |
| } |
| |
| // OnURLFetchComplete(). |
| if (!upload_complete_ && upload_listener_) { |
| upload_complete_ = true; |
| FireProgressEvent(upload_, base::Tokens::progress()); |
| FireProgressEvent(upload_, base::Tokens::load()); |
| FireProgressEvent(upload_, base::Tokens::loadend()); |
| } |
| ChangeState(XMLHttpRequest::kDone); |
| size_t received_length = response->size(); |
| FireProgressEvent(xhr_, base::Tokens::load(), received_length, |
| received_length, |
| /*length_computable=*/true); |
| FireProgressEvent(xhr_, base::Tokens::loadend(), received_length, |
| received_length, |
| /*length_computable=*/true); |
| // Undo the ref we added in Send() |
| DecrementActiveRequests(); |
| |
| fetch_callback_.reset(); |
| fetch_mode_callback_.reset(); |
| } |
| |
| void XMLHttpRequestImpl::SendFallback( |
| const base::Optional<XMLHttpRequest::RequestBodyType>& request_body, |
| script::ExceptionState* exception_state) { |
| if (will_destroy_current_message_loop_.load()) { |
| return; |
| } |
| if (task_runner_ != base::ThreadTaskRunnerHandle::Get()) { |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&XMLHttpRequestImpl::SendFallback, |
| base::Unretained(this), request_body, exception_state)); |
| return; |
| } |
| 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_ != XMLHttpRequest::kOpened) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return; |
| } |
| // Step 2 |
| if (sent_) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return; |
| } |
| |
| // Step 3 - 7 |
| error_ = false; |
| upload_complete_ = false; |
| |
| // 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_ = environment_settings()->GetOrigin(); |
| // 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(xhr_, 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_ == XMLHttpRequest::kOpened && sent_) { |
| this->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 XMLHttpRequestImpl::Fetch( |
| const FetchUpdateCallbackArg& fetch_callback, |
| const FetchModeCallbackArg& fetch_mode_callback, |
| const base::Optional<XMLHttpRequest::RequestBodyType>& request_body, |
| script::ExceptionState* exception_state) { |
| fetch_callback_.reset( |
| new FetchUpdateCallbackArg::Reference(xhr_, fetch_callback)); |
| fetch_mode_callback_.reset( |
| new FetchModeCallbackArg::Reference(xhr_, fetch_mode_callback)); |
| Send(request_body, exception_state); |
| } |
| |
| base::Optional<std::string> XMLHttpRequestImpl::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_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::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 XMLHttpRequestImpl::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_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::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& XMLHttpRequestImpl::response_text( |
| script::ExceptionState* exception_state) { |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsetext-attribute |
| if (response_type_ != XMLHttpRequest::kDefault && |
| response_type_ != XMLHttpRequest::kText) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| } |
| if (error_ || |
| (state_ != XMLHttpRequest::kLoading && state_ != XMLHttpRequest::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_ == XMLHttpRequest::kLoading) { |
| LOG_ONCE(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> XMLHttpRequestImpl::response_xml( |
| script::ExceptionState* exception_state) { |
| // Workers don't have access to DOM APIs, including Document objects. Nothing |
| // to do. |
| // https://www.w3.org/TR/2012/CR-workers-20120501/#apis-available-to-workers |
| return NULL; |
| } |
| |
| base::Optional<XMLHttpRequest::ResponseType> XMLHttpRequestImpl::response( |
| script::ExceptionState* exception_state) { |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#response |
| switch (response_type_) { |
| case XMLHttpRequest::kDefault: |
| case XMLHttpRequest::kText: |
| return XMLHttpRequest::ResponseType(response_text(exception_state)); |
| case XMLHttpRequest::kArrayBuffer: { |
| script::Handle<script::ArrayBuffer> maybe_array_buffer_response = |
| response_array_buffer(); |
| if (maybe_array_buffer_response.IsEmpty()) { |
| return base::nullopt; |
| } |
| return XMLHttpRequest::ResponseType(maybe_array_buffer_response); |
| } |
| case XMLHttpRequest::kJson: |
| case XMLHttpRequest::kDocument: |
| case XMLHttpRequest::kBlob: |
| case XMLHttpRequest::kResponseTypeCodeMax: |
| NOTIMPLEMENTED() << "Unsupported response_type_ " |
| << response_type(exception_state); |
| } |
| return base::nullopt; |
| } |
| |
| int XMLHttpRequestImpl::status() const { |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-status-attribute |
| if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened || |
| error_) { |
| return 0; |
| } else { |
| return http_status_; |
| } |
| } |
| |
| std::string XMLHttpRequestImpl::status_text() { |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-statustext-attribute |
| if (state_ == XMLHttpRequest::kUnsent || state_ == XMLHttpRequest::kOpened || |
| error_) { |
| return std::string(); |
| } |
| |
| return http_response_headers_->GetStatusText(); |
| } |
| |
| void XMLHttpRequestImpl::set_response_type( |
| const std::string& response_type, script::ExceptionState* exception_state) { |
| if (state_ == XMLHttpRequest::kLoading || state_ == XMLHttpRequest::kDone) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return; |
| } |
| for (size_t i = 0; i < arraysize(kResponseTypes); ++i) { |
| if (response_type == kResponseTypes[i]) { |
| DCHECK_LT(i, XMLHttpRequest::kResponseTypeCodeMax); |
| response_type_ = static_cast<XMLHttpRequest::ResponseTypeCode>(i); |
| return; |
| } |
| } |
| |
| DLOG(WARNING) << "Unexpected response type " << response_type; |
| } |
| |
| std::string XMLHttpRequestImpl::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 XMLHttpRequestImpl::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 XMLHttpRequestImpl::with_credentials( |
| script::ExceptionState* unused) const { |
| return with_credentials_; |
| } |
| |
| void XMLHttpRequestImpl::set_with_credentials( |
| bool with_credentials, script::ExceptionState* exception_state) { |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-withcredentials-attribute |
| if ((state_ != XMLHttpRequest::kUnsent && |
| state_ != XMLHttpRequest::kOpened) || |
| sent_) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return; |
| } |
| with_credentials_ = with_credentials; |
| } |
| |
| scoped_refptr<XMLHttpRequestUpload> XMLHttpRequestImpl::upload() { |
| if (!upload_) { |
| upload_ = new XMLHttpRequestUpload(environment_settings()); |
| } |
| return upload_; |
| } |
| |
| void XMLHttpRequestImpl::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(XMLHttpRequest::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_, cors_policy_)) { |
| HandleRequestError(XMLHttpRequest::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(XMLHttpRequest::kHeadersReceived); |
| |
| UpdateProgress(0); |
| } |
| |
| void XMLHttpRequestImpl::OnURLFetchDownloadProgress( |
| const net::URLFetcher* source, int64_t /*current*/, int64_t /*total*/, |
| int64_t /*current_network_bytes*/, bool request_done) { |
| TRACK_MEMORY_SCOPE("XHR"); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK_NE(state_, XMLHttpRequest::kDone); |
| |
| if (response_body_->HasProgressSinceLastGetAndReset(request_done) == 0) { |
| return; |
| } |
| |
| // Signal to JavaScript that new data is now available. |
| ChangeState(XMLHttpRequest::kLoading); |
| |
| if (fetch_callback_) { |
| if (response_body_->type() == URLFetcherResponseWriter::Buffer::kString) { |
| std::string downloaded_data; |
| response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data); |
| script::Handle<script::Uint8Array> data = script::Uint8Array::New( |
| environment_settings()->context()->global_environment(), |
| downloaded_data.data(), downloaded_data.size()); |
| fetch_callback_->value().Run(data); |
| } else { |
| DCHECK_EQ(response_body_->type(), |
| URLFetcherResponseWriter::Buffer::kBufferPool); |
| std::vector<script::PreallocatedArrayBufferData> downloaded_buffers; |
| response_body_->GetAndResetDataAndDownloadProgress(request_done, |
| &downloaded_buffers); |
| for (auto& downloaded_buffer : downloaded_buffers) { |
| auto array_buffer = |
| script::ArrayBuffer::New(settings_->context()->global_environment(), |
| std::move(downloaded_buffer)); |
| script::Handle<script::Uint8Array> data = script::Uint8Array::New( |
| settings_->context()->global_environment(), array_buffer, 0, |
| array_buffer->ByteLength()); |
| 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; |
| if (fetch_callback_) { |
| // TODO: Investigate if we have to fire progress event with 0 loaded bytes |
| // when used as Fetch API. |
| UpdateProgress(0); |
| } else { |
| UpdateProgress(response_body_->GetAndResetDownloadProgress()); |
| } |
| } |
| } |
| |
| |
| void XMLHttpRequestImpl::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; |
| } |
| // Create Performance Resource Timing entry after fetch complete. |
| this->GetLoadTimingInfoAndCreateResourceTiming(); |
| } |
| |
| 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 transferred to this XHR. This should |
| // only be done for successful and error-free fetches. |
| OnURLFetchDownloadProgress(source, 0, 0, 0, /* request_done = */ true); |
| |
| // 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(XMLHttpRequest::kDone); |
| UpdateProgress(response_body_->GetAndResetDownloadProgress()); |
| // Undo the ref we added in Send() |
| DecrementActiveRequests(); |
| } else { |
| HandleRequestError(XMLHttpRequest::kNetworkError); |
| } |
| |
| fetch_callback_.reset(); |
| fetch_mode_callback_.reset(); |
| } |
| |
| void XMLHttpRequestImpl::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 |
| // transmitted 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 XMLHttpRequestImpl::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(XMLHttpRequest::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(XMLHttpRequest::kNetworkError); |
| return; |
| } else if (is_cross_origin_) { |
| DLOG(INFO) << "XHR is redirected with credentials and cors_flag set, " |
| "aborting request for security reasons."; |
| HandleRequestError(XMLHttpRequest::kNetworkError); |
| return; |
| } |
| } |
| if (!new_url.is_valid()) { |
| HandleRequestError(XMLHttpRequest::kNetworkError); |
| return; |
| } |
| // This is a redirect. Re-check the CSP. |
| web::CspDelegate* csp = csp_delegate(); |
| if (csp && |
| !csp->CanLoad(web::CspDelegate::kXhr, new_url, true /* is_redirect */)) { |
| HandleRequestError(XMLHttpRequest::kNetworkError); |
| return; |
| } |
| // CORS check for the received response |
| if (is_cross_origin_) { |
| if (!loader::CORSPreflight::CORSCheck(headers, origin_.SerializedOrigin(), |
| with_credentials_, cors_policy_)) { |
| HandleRequestError(XMLHttpRequest::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_++; |
| this->StartRequest(request_body_text_); |
| } |
| |
| void XMLHttpRequestImpl::WillDestroyCurrentMessageLoop() { |
| will_destroy_current_message_loop_.store(true); |
| } |
| |
| void XMLHttpRequestImpl::ReportLoadTimingInfo( |
| const net::LoadTimingInfo& timing_info) { |
| load_timing_info_ = timing_info; |
| } |
| |
| void XMLHttpRequestImpl::GetLoadTimingInfoAndCreateResourceTiming() { |
| // Performance info is only available through window currently. Not available |
| // in workers. |
| return; |
| } |
| |
| void XMLHttpRequestImpl::TraceMembers(script::Tracer* tracer) { |
| tracer->Trace(upload_); |
| } |
| |
| web::CspDelegate* XMLHttpRequestImpl::csp_delegate() const { |
| return environment_settings() |
| ->context() |
| ->GetWindowOrWorkerGlobalScope() |
| ->csp_delegate(); |
| } |
| |
| void XMLHttpRequestImpl::TerminateRequest() { |
| error_ = true; |
| corspreflight_.reset(NULL); |
| url_fetcher_.reset(NULL); |
| } |
| |
| void XMLHttpRequestImpl::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) << ") " |
| << *xhr_ << std::endl |
| << script::StackTraceToString( |
| environment_settings() |
| ->context() |
| ->global_environment() |
| ->GetStackTrace(0 /*max_frames*/)); |
| stop_timeout_ = true; |
| // Step 1 |
| TerminateRequest(); |
| // Steps 2-4 |
| // Change state and fire readystatechange event. |
| ChangeState(XMLHttpRequest::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(xhr_, base::Tokens::progress()); |
| FireProgressEvent(xhr_, error_name); |
| FireProgressEvent(xhr_, base::Tokens::loadend()); |
| |
| fetch_callback_.reset(); |
| fetch_mode_callback_.reset(); |
| DecrementActiveRequests(); |
| } |
| |
| void XMLHttpRequestImpl::OnTimeout() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (!stop_timeout_) { |
| HandleRequestError(XMLHttpRequest::kTimeoutError); |
| } |
| } |
| |
| void XMLHttpRequestImpl::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, &XMLHttpRequestImpl::OnTimeout); |
| } |
| |
| void XMLHttpRequestImpl::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 != XMLHttpRequest::kLoading) { |
| return; |
| } |
| |
| state_ = new_state; |
| if (state_ != XMLHttpRequest::kUnsent) { |
| xhr_->DispatchEvent(new web::Event(base::Tokens::readystatechange())); |
| } |
| } |
| |
| script::Handle<script::ArrayBuffer> |
| XMLHttpRequestImpl::response_array_buffer() { |
| TRACK_MEMORY_SCOPE("XHR"); |
| // https://www.w3.org/TR/XMLHttpRequest/#response-entity-body |
| if (error_ || state_ != XMLHttpRequest::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. |
| script::PreallocatedArrayBufferData downloaded_data; |
| response_body_->GetAndResetData(&downloaded_data); |
| auto array_buffer = script::ArrayBuffer::New( |
| environment_settings()->context()->global_environment(), |
| std::move(downloaded_data)); |
| response_array_buffer_reference_.reset( |
| new script::ScriptValue<script::ArrayBuffer>::Reference(xhr_, |
| array_buffer)); |
| return array_buffer; |
| } else { |
| return script::Handle<script::ArrayBuffer>( |
| *response_array_buffer_reference_); |
| } |
| } |
| |
| void XMLHttpRequestImpl::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 << ") " << *xhr_; |
| |
| if (state_ == XMLHttpRequest::kDone) { |
| FireProgressEvent(xhr_, base::Tokens::load(), |
| static_cast<uint64>(received_length), total, |
| length_computable); |
| FireProgressEvent(xhr_, base::Tokens::loadend(), |
| static_cast<uint64>(received_length), total, |
| length_computable); |
| } else { |
| FireProgressEvent(xhr_, base::Tokens::progress(), |
| static_cast<uint64>(received_length), total, |
| length_computable); |
| } |
| } |
| |
| void XMLHttpRequestImpl::StartRequest(const std::string& request_body) { |
| TRACK_MEMORY_SCOPE("XHR"); |
| |
| const auto& xhr_settings = |
| environment_settings()->context()->web_settings()->xhr_settings(); |
| const bool fetch_buffer_pool_enabled = |
| xhr_settings.IsFetchBufferPoolEnabled().value_or(false); |
| |
| if (fetch_callback_ && fetch_buffer_pool_enabled) { |
| LOG(INFO) << "Fetching (backed by BufferPool): " |
| << ClipUrl(request_url_, 200); |
| } else { |
| LOG(INFO) << "Fetching: " << ClipUrl(request_url_, 200); |
| } |
| |
| response_array_buffer_reference_.reset(); |
| |
| network::NetworkModule* network_module = |
| environment_settings()->context()->fetcher_factory()->network_module(); |
| url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_); |
| ++url_fetcher_generation_; |
| url_fetcher_->SetRequestContext(network_module->url_request_context_getter()); |
| |
| if (fetch_callback_) { |
| // FetchBufferPool is by default disabled, but can be explicitly overridden. |
| if (fetch_buffer_pool_enabled) { |
| response_body_ = new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kBufferPool, |
| xhr_settings.IsTryLockForProgressCheckEnabled(), |
| xhr_settings.GetDefaultFetchBufferSize()); |
| } else { |
| response_body_ = new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kString, |
| xhr_settings.IsTryLockForProgressCheckEnabled()); |
| response_body_->DisablePreallocate(); |
| } |
| } else { |
| if (response_type_ == XMLHttpRequest::kArrayBuffer) { |
| response_body_ = new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kArrayBuffer, |
| xhr_settings.IsTryLockForProgressCheckEnabled()); |
| } else { |
| response_body_ = new URLFetcherResponseWriter::Buffer( |
| URLFetcherResponseWriter::Buffer::kString, |
| xhr_settings.IsTryLockForProgressCheckEnabled()); |
| } |
| } |
| 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()); |
| network_module->AddClientHintHeaders(*url_fetcher_); |
| |
| // 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(&DOMXMLHttpRequestImpl::CORSPreflightSuccessCallback, |
| base::Unretained(this)), |
| origin_.SerializedOrigin(), |
| base::Bind(&DOMXMLHttpRequestImpl::CORSPreflightErrorCallback, |
| base::Unretained(this)), |
| environment_settings() |
| ->context() |
| ->GetWindowOrWorkerGlobalScope() |
| ->get_preflight_cache())); |
| corspreflight_->set_headers(request_headers_); |
| corspreflight_->set_cors_policy(cors_policy_); |
| // 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__ << *xhr_; |
| if (!dopreflight) { |
| DCHECK(environment_settings()->context()->network_module()); |
| StartURLFetcher(environment_settings() |
| ->context() |
| ->network_module() |
| ->max_network_delay(), |
| url_fetcher_generation_); |
| } |
| } |
| |
| void XMLHttpRequestImpl::IncrementActiveRequests() { |
| if (active_requests_count_ == 0) { |
| prevent_gc_until_send_complete_.reset( |
| new script::GlobalEnvironment::ScopedPreventGarbageCollection( |
| environment_settings()->context()->global_environment(), xhr_)); |
| } |
| active_requests_count_++; |
| } |
| |
| void XMLHttpRequestImpl::DecrementActiveRequests() { |
| DCHECK_GT(active_requests_count_, 0); |
| active_requests_count_--; |
| if (active_requests_count_ == 0) { |
| bool is_active = (state_ == XMLHttpRequest::kOpened && sent_) || |
| state_ == XMLHttpRequest::kHeadersReceived || |
| state_ == XMLHttpRequest::kLoading; |
| bool has_event_listeners = |
| xhr_->GetAttributeEventListener(base::Tokens::readystatechange()) || |
| xhr_->GetAttributeEventListener(base::Tokens::progress()) || |
| xhr_->GetAttributeEventListener(base::Tokens::abort()) || |
| xhr_->GetAttributeEventListener(base::Tokens::error()) || |
| xhr_->GetAttributeEventListener(base::Tokens::load()) || |
| xhr_->GetAttributeEventListener(base::Tokens::timeout()) || |
| xhr_->GetAttributeEventListener(base::Tokens::loadend()); |
| |
| DCHECK_EQ((is_active && has_event_listeners), false); |
| |
| prevent_gc_until_send_complete_.reset(); |
| } |
| } |
| |
| // Reset some variables in case the XHR object is reused. |
| void XMLHttpRequestImpl::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 XMLHttpRequestImpl::StartURLFetcher(const SbTime max_artificial_delay, |
| const int url_fetcher_generation) { |
| if (max_artificial_delay > 0) { |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&XMLHttpRequestImpl::StartURLFetcher, base::Unretained(this), |
| 0, url_fetcher_generation_), |
| base::TimeDelta::FromMicroseconds(base::RandUint64() % |
| max_artificial_delay)); |
| return; |
| } |
| |
| // Note: Checking that "url_fetcher_generation_" != "url_fetcher_generation" |
| // is to verify the "url_fetcher_" is currently the same one that was present |
| // upon a delayed url fetch. This works because the incoming parameter |
| // "url_fetcher_generation" will hold the value at the time of the initial |
| // call, and if a delayed binding has waited while a new "url_fetcher_" has |
| // changed state, "url_fetcher_generation_" will have incremented. |
| if (nullptr != url_fetcher_ && |
| url_fetcher_generation == url_fetcher_generation_) { |
| url_fetcher_->Start(); |
| } |
| } |
| |
| void XMLHttpRequestImpl::CORSPreflightErrorCallback() { |
| HandleRequestError(XMLHttpRequest::kNetworkError); |
| } |
| |
| void XMLHttpRequestImpl::CORSPreflightSuccessCallback() { |
| DCHECK(environment_settings()->context()->network_module()); |
| StartURLFetcher( |
| environment_settings()->context()->network_module()->max_network_delay(), |
| url_fetcher_generation_); |
| } |
| |
| DOMXMLHttpRequestImpl::DOMXMLHttpRequestImpl(XMLHttpRequest* xhr) |
| : XMLHttpRequestImpl(xhr) { |
| DCHECK(environment_settings()); |
| } |
| |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsexml-attribute |
| scoped_refptr<dom::Document> DOMXMLHttpRequestImpl::response_xml( |
| script::ExceptionState* exception_state) { |
| // 1. If responseType is not the empty string or "document", throw an |
| // "InvalidStateError" exception. |
| if (response_type_ != XMLHttpRequest::kDefault && |
| response_type_ != XMLHttpRequest::kDocument) { |
| web::DOMException::Raise(web::DOMException::kInvalidStateErr, |
| exception_state); |
| return NULL; |
| } |
| |
| // 2. If the state is not DONE, return null. |
| if (state_ != XMLHttpRequest::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(); |
| } |
| |
| void DOMXMLHttpRequestImpl::GetLoadTimingInfoAndCreateResourceTiming() { |
| web::WindowOrWorkerGlobalScope* window_or_worker_global_scope = |
| environment_settings()->context()->GetWindowOrWorkerGlobalScope(); |
| if (!window_or_worker_global_scope->IsWindow()) return; |
| if (window_or_worker_global_scope->AsWindow()->performance() == nullptr) |
| return; |
| window_or_worker_global_scope->AsWindow() |
| ->performance() |
| ->CreatePerformanceResourceTiming(load_timing_info_, |
| kPerformanceResourceTimingInitiatorType, |
| request_url_.spec()); |
| } |
| |
| // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#document-response-entity-body |
| scoped_refptr<dom::Document> |
| DOMXMLHttpRequestImpl::GetDocumentResponseEntityBody() { |
| DCHECK_EQ(state_, XMLHttpRequest::kDone); |
| if (!environment_settings() |
| ->context() |
| ->GetWindowOrWorkerGlobalScope() |
| ->IsWindow()) { |
| return nullptr; |
| } |
| |
| // 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 nullptr; |
| } |
| |
| // 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(environment_settings() |
| ->context() |
| ->GetWindowOrWorkerGlobalScope() |
| ->AsWindow() |
| ->html_element_context()); |
| dom_parser::XMLDecoder xml_decoder( |
| xml_document, xml_document, NULL, |
| base::polymorphic_downcast<dom::DOMSettings*>(environment_settings()) |
| ->max_dom_element_depth(), |
| base::SourceLocation("[object XMLHttpRequest]", 1, 1), |
| base::Bind(&DOMXMLHttpRequestImpl::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 DOMXMLHttpRequestImpl::XMLDecoderLoadCompleteCallback( |
| const base::Optional<std::string>& error) { |
| if (error) has_xml_decoder_error_ = true; |
| } |
| |
| std::ostream& operator<<(std::ostream& out, const XMLHttpRequest& xhr) { |
| #if !defined(COBALT_BUILD_TYPE_GOLD) |
| base::StringPiece response_text(""); |
| if ((xhr.xhr_impl_->state_ == XMLHttpRequest::kDone) && |
| (xhr.xhr_impl_->response_type_ == XMLHttpRequest::kDefault || |
| xhr.xhr_impl_->response_type_ == XMLHttpRequest::kText)) { |
| size_t kMaxSize = 4096; |
| const auto& response_body = |
| xhr.xhr_impl_->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.xhr_impl_->request_url_.spec().c_str(), |
| StateName(xhr.xhr_impl_->state_), xhr.response_type(NULL).c_str(), |
| xhr.xhr_impl_->timeout_ms_, |
| RequestTypeToMethodName(xhr.xhr_impl_->method_), |
| xhr.xhr_impl_->http_status_, |
| xhr.xhr_impl_->with_credentials_ ? "true" : "false", |
| xhr.xhr_impl_->error_ ? "true" : "false", |
| xhr.xhr_impl_->sent_ ? "true" : "false", |
| xhr.xhr_impl_->stop_timeout_ ? "true" : "false", |
| response_text.as_string().c_str()); |
| out << xhr_out; |
| #else |
| #endif |
| return out; |
| } |
| |
| } // namespace xhr |
| } // namespace cobalt |