Shaka Player Embedded
xml_http_request.cc
Go to the documentation of this file.
1 // Copyright 2016 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
16 
17 #include <algorithm>
18 #include <cctype>
19 #include <cerrno>
20 #include <cstring>
21 #include <stdexcept>
22 #include <utility>
23 
24 #include "src/core/environment.h"
26 #include "src/js/events/event.h"
29 #include "src/js/js_error.h"
30 #include "src/js/navigator.h"
31 #include "src/js/timeouts.h"
32 #include "src/memory/heap_tracer.h"
33 #include "src/util/clock.h"
34 #include "src/util/utils.h"
35 
36 namespace shaka {
37 namespace js {
38 
39 namespace {
40 
42 constexpr size_t kProgressInterval = 15;
43 
44 constexpr const char* kCookieFileName = "net_cookies.dat";
45 
46 size_t UploadCallback(void* buffer, size_t member_size, size_t member_count,
47  void* user_data) {
48  auto* request = reinterpret_cast<XMLHttpRequest*>(user_data);
49  auto* buffer_bytes = reinterpret_cast<uint8_t*>(buffer);
50  size_t total_size = member_size * member_count;
51  return request->OnUpload(buffer_bytes, total_size);
52 }
53 
54 size_t DownloadCallback(void* buffer, size_t member_size, size_t member_count,
55  void* user_data) {
56  auto* request = reinterpret_cast<XMLHttpRequest*>(user_data);
57  auto* buffer_bytes = reinterpret_cast<uint8_t*>(buffer);
58  size_t total_size = member_size * member_count;
59  request->OnDataReceived(buffer_bytes, total_size);
60  return total_size;
61 }
62 
63 size_t HeaderCallback(char* buffer, size_t member_size, size_t member_count,
64  void* user_data) {
65  auto* request = reinterpret_cast<XMLHttpRequest*>(user_data);
66  auto* buffer_bytes = reinterpret_cast<uint8_t*>(buffer);
67  size_t total_size = member_size * member_count;
68  request->OnHeaderReceived(buffer_bytes, total_size);
69  return total_size;
70 }
71 
72 double CurrentDownloadSize(CURL* curl) {
73  double ret;
74  CHECK_EQ(CURLE_OK, curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &ret));
75  return ret;
76 }
77 
90 bool ParseStatusLine(const char* buffer, size_t length, int* code,
91  const char** message, size_t* message_length) {
92  // NOTE: curl will convert the status line of http2 to match http1.1.
93  // e.g.: "HTTP/1.1 200 OK\r\n"
94  constexpr const char kMinStatusLine[] = "HTTP/1.1 200 \r\n";
95  constexpr const char kHttp10[] = "HTTP/1.0 ";
96  constexpr const char kHttp11[] = "HTTP/1.1 ";
97  constexpr const size_t kMinStatusSize = sizeof(kMinStatusLine) - 1;
98  constexpr const size_t kHttp10Size = sizeof(kHttp10) - 1;
99  constexpr const size_t kHttp11Size = sizeof(kHttp11) - 1;
100  constexpr const size_t kStatusSize = 3;
101 
102  if (length < kMinStatusSize)
103  return false;
104  if (strncmp(buffer, kHttp10, kHttp10Size) != 0 &&
105  strncmp(buffer, kHttp11, kHttp11Size) != 0)
106  return false;
107 
108  // Convert the status code, must be three numbers [0-9].
109  const char* status_code = buffer + kHttp10Size;
110  const char* status_code_end = status_code + kStatusSize;
111  if (!std::isdigit(status_code[0]) || !std::isdigit(status_code[1]) ||
112  !std::isdigit(status_code[2])) {
113  return false;
114  }
115  if (status_code_end[0] != ' ')
116  return false;
117  if (buffer[length - 2] != '\r' || buffer[length - 1] != '\n')
118  return false;
119 
120  *code = std::stoi(std::string(status_code, status_code_end));
121  *message = status_code_end + 1;
122  *message_length = length - kMinStatusSize;
123  return true;
124 }
125 
126 } // namespace
127 
129  : ready_state(XMLHttpRequest::ReadyState::Unsent),
130  mutex_("XMLHttpRequest"),
131  curl_(curl_easy_init()),
132  request_headers_(nullptr),
133  with_credentials_(false) {
134  AddListenerField(EventType::Abort, &on_abort);
135  AddListenerField(EventType::Error, &on_error);
136  AddListenerField(EventType::Load, &on_load);
137  AddListenerField(EventType::LoadStart, &on_load_start);
138  AddListenerField(EventType::Progress, &on_progress);
139  AddListenerField(EventType::ReadyStateChange, &on_ready_state_change);
140  AddListenerField(EventType::Timeout, &on_timeout);
141  AddListenerField(EventType::LoadEnd, &on_load_end);
142 
143  Reset();
144 }
145 
146 // \cond Doxygen_Skip
147 XMLHttpRequest::~XMLHttpRequest() {
148  // Don't call Abort() since we can't raise events.
149  abort_pending_ = true;
151 
152  curl_easy_cleanup(curl_);
153  if (request_headers_)
154  curl_slist_free_all(request_headers_);
155  request_headers_ = nullptr;
156 }
157 // \endcond Doxygen_Skip
158 
160  // No need to trace on_* members as EventTarget handles it.
161  EventTarget::Trace(tracer);
162  std::unique_lock<Mutex> lock(mutex_);
163  tracer->Trace(&response);
164  tracer->Trace(&upload_data_);
165 }
166 
168  return true;
169 }
170 
172  if (!JsManagerImpl::Instance()->NetworkThread()->ContainsRequest(this))
173  return;
174 
175  abort_pending_ = true;
177 
178  std::unique_lock<Mutex> lock(mutex_);
180  // Fire events synchronously.
182  RaiseEvent<events::Event>(EventType::ReadyStateChange);
183 
184  double total_size = CurrentDownloadSize(curl_);
185  RaiseEvent<events::ProgressEvent>(EventType::Progress, true, total_size,
186  total_size);
187  RaiseEvent<events::Event>(EventType::Abort);
188  RaiseEvent<events::ProgressEvent>(EventType::LoadEnd, true, total_size,
189  total_size);
190  }
191 
192  // The spec says at the end to change the ready state without firing an event.
193  // https://xhr.spec.whatwg.org/#the-abort()-method
195 }
196 
198  std::unique_lock<Mutex> lock(mutex_);
199  std::string ret;
200  for (auto it : response_headers_) {
201  ret.append(it.first + ": " + it.second + "\r\n");
202  }
203  return ret;
204 }
205 
207  const std::string& name) const {
208  std::unique_lock<Mutex> lock(mutex_);
209  auto it = response_headers_.find(name);
210  if (it == response_headers_.end())
211  return nullopt;
212  return it->second;
213 }
214 
215 ExceptionOr<void> XMLHttpRequest::Open(const std::string& method,
216  const std::string& url,
217  optional<bool> async,
219  optional<std::string> password) {
220  if (async.has_value() && !async.value()) {
222  "Synchronous requests are not supported.");
223  }
224 
225  // This will call Abort() which may call back into JavaScript by firing
226  // events synchronously.
227  Reset();
228 
229  std::unique_lock<Mutex> lock(mutex_);
231  ScheduleEvent<events::Event>(EventType::ReadyStateChange);
232 
233  curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
234  curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, method.c_str());
235  if (method == "HEAD")
236  curl_easy_setopt(curl_, CURLOPT_NOBODY, 1L);
237 
238  if (user.has_value())
239  curl_easy_setopt(curl_, CURLOPT_USERNAME, user->c_str());
240  if (password.has_value())
241  curl_easy_setopt(curl_, CURLOPT_PASSWORD, password->c_str());
242 
243  return {};
244 }
245 
248  // Don't query while locked to avoid a deadlock.
249  const bool contains_request =
251 
252  {
253  std::unique_lock<Mutex> lock(mutex_);
254  // If we are not open, or if the request has already been sent.
255  if (ready_state != XMLHttpRequest::ReadyState::Opened || contains_request) {
257  "The object's state must be OPENED.");
258  }
259  if (response_type != "arraybuffer") {
260  return JsError::DOMException(
262  "Response type " + response_type + " is not supported");
263  }
264 
265  if (maybe_data.has_value()) {
266  if (holds_alternative<ByteBuffer>(*maybe_data)) {
267  upload_data_ = std::move(get<ByteBuffer>(*maybe_data));
268  } else {
269  ByteString* str = &get<ByteString>(*maybe_data);
270  upload_data_.SetFromBuffer(str->data(), str->size());
271  }
272 
273  upload_pos_ = 0;
274  curl_easy_setopt(curl_, CURLOPT_UPLOAD, 1L);
275  curl_easy_setopt(curl_, CURLOPT_INFILESIZE_LARGE,
276  static_cast<curl_off_t>(upload_data_.size()));
277  curl_easy_setopt(curl_, CURLOPT_READDATA, this);
278  curl_easy_setopt(curl_, CURLOPT_READFUNCTION, UploadCallback);
279  } else {
280  curl_easy_setopt(curl_, CURLOPT_UPLOAD, 0L);
281  }
282 
283  curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS,
284  static_cast<long>(timeout_ms)); // NOLINT
285  curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, request_headers_);
286  }
287 
288  // Don't add while locked to avoid a deadlock.
290  return {};
291 }
292 
294  const std::string& value) {
295  std::unique_lock<Mutex> lock(mutex_);
298  "The object's state must be OPENED.");
299  }
300  const std::string header = key + ": " + value;
301  request_headers_ = curl_slist_append(request_headers_, header.c_str());
302  return {};
303 }
304 
306  return with_credentials_;
307 }
308 
312  return JsError::DOMException(
314  "withCredentials can only be set if the object's state is UNSENT or "
315  "OPENED.");
316  }
317  with_credentials_ = with_credentials;
318  return {};
319 }
320 
321 void XMLHttpRequest::RaiseProgressEvents() {
322  if (abort_pending_)
323  return;
324 
327  RaiseEvent<events::Event>(EventType::ReadyStateChange);
328  }
331  RaiseEvent<events::Event>(EventType::ReadyStateChange);
332  }
333 
334  double cur_size;
335  {
336  std::unique_lock<Mutex> lock(mutex_);
337  cur_size = CurrentDownloadSize(curl_);
338  }
339  RaiseEvent<events::ProgressEvent>(EventType::Progress, estimated_size_ != 0,
340  cur_size, estimated_size_);
341 }
342 
343 void XMLHttpRequest::OnDataReceived(uint8_t* buffer, size_t length) {
344  std::unique_lock<Mutex> lock(mutex_);
345 
346  // We need to schedule these events from this callback since we don't know
347  // when the last header will be received.
348  const uint64_t now = util::Clock::Instance.GetMonotonicTime();
349  if (!abort_pending_ && now - last_progress_time_ >= kProgressInterval) {
350  last_progress_time_ = now;
351 
352  RefPtr<XMLHttpRequest> req(this);
354  TaskPriority::Internal, "Schedule XHR events",
355  std::bind(&XMLHttpRequest::RaiseProgressEvents, req));
356  }
357 
358  temp_data_.AppendCopy(buffer, length);
359 }
360 
361 void XMLHttpRequest::OnHeaderReceived(const uint8_t* buffer, size_t length) {
362  std::unique_lock<Mutex> lock(mutex_);
363  // This method is called for each header (including status line) for the
364  // duration of the request, this includes redirects.
365  // https://curl.haxx.se/libcurl/c/CURLOPT_HEADERFUNCTION.html
366  //
367  // Be careful about using string methods. This data may not be null-
368  // terminated. Be sure to use the |length| parameter and always pass the
369  // explicit end to std::string.
370  auto* str = reinterpret_cast<const char*>(buffer);
371  if (!parsing_headers_) {
372  const char* message;
373  size_t message_size;
374  if (!ParseStatusLine(str, length, &status, &message, &message_size))
375  return;
376  status_text.assign(message, message + message_size);
377 
378  parsing_headers_ = true;
379  // Clear headers from the previous request. This is important for
380  // redirects so we don't keep headers from the redirect.
381  response_headers_.clear();
382  } else {
383  // 'Content-Length: 123\r\n'
384  const char* sep = std::find(str, str + length, ':');
385  // |sep == str + length| if not found.
386  const size_t key_len = sep - str;
387  const size_t rest_len = length - key_len; // == 0 if not found.
388  if (rest_len >= 2 && sep[rest_len - 2] == '\r' &&
389  sep[rest_len - 1] == '\n') {
390  std::string key = util::ToAsciiLower(std::string(str, sep));
391  std::string value =
392  util::TrimAsciiWhitespace(std::string(sep + 1, sep + rest_len - 1));
393 
394  if (response_headers_.count(key) == 0)
395  response_headers_[key] = value;
396  else
397  response_headers_[key] += ", " + value;
398 
399  // Parse content-length so we can get the size of the download.
400  if (key == "content-length") {
401  errno = 0; // |errno| is thread_local.
402  char* end;
403  const auto size = strtol(value.c_str(), &end, 10);
404  if (errno != ERANGE && end == value.c_str() + value.size()) {
405  estimated_size_ = size;
406  }
407  }
408  }
409  // Ignore invalid headers.
410  }
411  if (length == 2 && str[0] == '\r' && str[1] == '\n') {
412  // An empty header signals the end of the headers for the current request.
413  // If there is a redirect or the XMLHttpRequest object is used to make
414  // another request then we should parse the status line.
415  parsing_headers_ = false;
416  }
417 }
418 
419 size_t XMLHttpRequest::OnUpload(uint8_t* buffer, size_t length) {
420  std::unique_lock<Mutex> lock(mutex_);
421  const size_t remaining = upload_data_.size() - upload_pos_;
422  if (length > remaining)
423  length = remaining;
424  std::memcpy(buffer, upload_data_.data() + upload_pos_, length);
425  upload_pos_ += length;
426  return length;
427 }
428 
429 void XMLHttpRequest::Reset() {
430  Abort();
431  response.Clear();
432  response_text = "";
433  response_type = "arraybuffer";
434  response_url = "";
435  status = 0;
436  status_text = "";
437  timeout_ms = 0;
438 
439  last_progress_time_ = 0;
440  estimated_size_ = 0;
441  parsing_headers_ = false;
442  abort_pending_ = false;
443 
444  response_headers_.clear();
445  temp_data_.Clear();
446  upload_data_.Clear();
447 
448  curl_easy_reset(curl_);
449  curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, DownloadCallback);
450  curl_easy_setopt(curl_, CURLOPT_WRITEDATA, this);
451  curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION, HeaderCallback);
452  curl_easy_setopt(curl_, CURLOPT_HEADERDATA, this);
453  curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L);
454  curl_easy_setopt(curl_, CURLOPT_USERAGENT, USER_AGENT);
455 
456  const std::string cookie_file =
458  curl_easy_setopt(curl_, CURLOPT_COOKIEFILE, cookie_file.c_str());
459  curl_easy_setopt(curl_, CURLOPT_COOKIEJAR, cookie_file.c_str());
460 
461  // Don't batch up TCP packets.
462  curl_easy_setopt(curl_, CURLOPT_TCP_NODELAY, 1L);
463  // Don't wait for a 100 Continue for uploads.
464  curl_easy_setopt(curl_, CURLOPT_EXPECT_100_TIMEOUT_MS, 1L);
465 
466  if (request_headers_)
467  curl_slist_free_all(request_headers_);
468  request_headers_ = nullptr;
469 }
470 
471 void XMLHttpRequest::OnRequestComplete(CURLcode code) {
472  // Careful, this is called from the worker thread, so we cannot call into V8.
473  std::unique_lock<Mutex> lock(mutex_);
474  if (code == CURLE_OK) {
475  response_text = temp_data_.CreateString();
476  response.SetFromDynamicBuffer(temp_data_);
477  temp_data_.Clear();
478 
479  char* url;
480  curl_easy_getinfo(curl_, CURLINFO_EFFECTIVE_URL, &url);
481  response_url = url;
482 
483  // Flush cookie list to disk so other instances can access them.
484  curl_easy_setopt(curl_, CURLOPT_COOKIELIST, "FLUSH");
485  } else {
486  // Don't need to reset everything on error because it was reset in Send().
487  // But we do need to set these as they are set in OnHeaderReceived.
488  status = 0;
489  status_text = "";
490  }
491 
492  // Don't schedule events if we are aborted, they will be called within
493  // Abort().
494  if (!abort_pending_) {
496  ScheduleEvent<events::Event>(EventType::ReadyStateChange);
497 
498  double total_size = CurrentDownloadSize(curl_);
499  ScheduleEvent<events::ProgressEvent>(EventType::Progress, true, total_size,
500  total_size);
501  switch (code) {
502  case CURLE_OK:
503  ScheduleEvent<events::Event>(EventType::Load);
504  break;
505  case CURLE_OPERATION_TIMEDOUT:
506  ScheduleEvent<events::Event>(EventType::Timeout);
507  break;
508  default:
509  LOG(ERROR) << "Error returned by curl: " << code;
510  ScheduleEvent<events::Event>(EventType::Error);
511  break;
512  }
513  ScheduleEvent<events::ProgressEvent>(EventType::LoadEnd, true, total_size,
514  total_size);
515  }
516 }
517 
518 
520  AddConstant("UNSENT", XMLHttpRequest::ReadyState::Unsent);
521  AddConstant("OPENED", XMLHttpRequest::ReadyState::Opened);
522  AddConstant("HEADERS_RECEIVED", XMLHttpRequest::ReadyState::HeadersReceived);
523  AddConstant("LOADING", XMLHttpRequest::ReadyState::Loading);
524  AddConstant("DONE", XMLHttpRequest::ReadyState::Done);
525 
526  AddReadOnlyProperty("readyState", &XMLHttpRequest::ready_state);
527  AddReadOnlyProperty("response", &XMLHttpRequest::response);
528  AddReadOnlyProperty("responseText", &XMLHttpRequest::response_text);
529  AddReadWriteProperty("responseType", &XMLHttpRequest::response_type);
530  AddReadOnlyProperty("responseURL", &XMLHttpRequest::response_url);
531  AddReadOnlyProperty("status", &XMLHttpRequest::status);
532  AddReadOnlyProperty("statusText", &XMLHttpRequest::status_text);
533  AddReadWriteProperty("timeout", &XMLHttpRequest::timeout_ms);
534  AddGenericProperty("withCredentials", &XMLHttpRequest::WithCredentials,
536 
537  AddListenerField(EventType::Abort, &XMLHttpRequest::on_abort);
538  AddListenerField(EventType::Error, &XMLHttpRequest::on_error);
539  AddListenerField(EventType::Load, &XMLHttpRequest::on_load);
540  AddListenerField(EventType::LoadStart, &XMLHttpRequest::on_load_start);
541  AddListenerField(EventType::Progress, &XMLHttpRequest::on_progress);
542  AddListenerField(EventType::ReadyStateChange,
544  AddListenerField(EventType::Timeout, &XMLHttpRequest::on_timeout);
545  AddListenerField(EventType::LoadEnd, &XMLHttpRequest::on_load_end);
546 
547  AddMemberFunction("abort", &XMLHttpRequest::Abort);
548  AddMemberFunction("getAllResponseHeaders",
550  AddMemberFunction("getResponseHeader", &XMLHttpRequest::GetResponseHeader);
551  AddMemberFunction("open", &XMLHttpRequest::Open);
552  AddMemberFunction("send", &XMLHttpRequest::Send);
553  AddMemberFunction("setRequestHeader", &XMLHttpRequest::SetRequestHeader);
554 }
555 
556 } // namespace js
557 } // namespace shaka
void SetFromDynamicBuffer(const util::DynamicBuffer &other)
Definition: byte_buffer.cc:72
std::string ToAsciiLower(const std::string &source)
Definition: utils.cc:76
bool ContainsRequest(RefPtr< js::XMLHttpRequest > request) const
std::string GetPathForDynamicFile(const std::string &file) const
std::string GetAllResponseHeaders() const
const uint8_t * data() const
Definition: byte_buffer.h:50
const nullopt_t nullopt
Definition: optional.cc:22
const T & value() const &
Definition: optional.h:147
void Trace(memory::HeapTracer *tracer) const override
void Trace(const Traceable *ptr)
Definition: heap_tracer.cc:43
std::string CreateString() const
optional< std::string > GetResponseHeader(const std::string &name) const
void SetFromBuffer(const void *buffer, size_t size)
Definition: byte_buffer.cc:77
void AbortRequest(RefPtr< js::XMLHttpRequest > request)
size_t size() const
Definition: byte_buffer.h:55
NetworkThread * NetworkThread()
void OnHeaderReceived(const uint8_t *buffer, size_t length)
void AppendCopy(const void *buffer, size_t size)
static const Clock Instance
Definition: clock.h:29
ExceptionOr< void > Open(const std::string &method, const std::string &url, optional< bool > async, optional< std::string > user, optional< std::string > password)
void AddRequest(RefPtr< js::XMLHttpRequest > request)
std::shared_ptr< ThreadEvent< impl::RetOf< Func > > > AddInternalTask(TaskPriority priority, const std::string &name, Func &&callback)
Definition: task_runner.h:219
bool IsShortLived() const override
std::string TrimAsciiWhitespace(const std::string &source)
Definition: utils.cc:82
static JsError DOMException(ExceptionCode code)
Definition: js_error.cc:115
virtual uint64_t GetMonotonicTime() const
Definition: clock.cc:29
const char * message
ExceptionOr< void > SetWithCredentials(bool with_credentials)
ExceptionOr< void > Send(optional< variant< ByteBuffer, ByteString >> maybe_data)
bool has_value() const
Definition: optional.h:143
void AddListenerField(EventType type, Listener *on_field)
Definition: event_target.h:138
void OnDataReceived(uint8_t *buffer, size_t length)
size_t OnUpload(uint8_t *buffer, size_t length)
std::string name() const
ExceptionOr< void > SetRequestHeader(const std::string &key, const std::string &value)
TaskRunner * MainThread()