// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


#include "third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.h"

#include <stddef.h>

#include <utility>

#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "net/http/http_byte_range.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/mojom/cors.mojom-blink.h"
#include "services/network/public/mojom/fetch_api.mojom-blink.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/platform/web_network_state_notifier.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/platform/web_url_error.h"
#include "third_party/blink/public/platform/web_url_response.h"
#include "third_party/blink/public/web/web_associated_url_loader.h"
#include "third_party/blink/renderer/platform/media/cache_util.h"
#include "third_party/blink/renderer/platform/media/resource_fetch_context.h"
#include "third_party/blink/renderer/platform/media/url_index.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

namespace blink {

// The number of milliseconds to wait before retrying a failed load.
const int kLoaderFailedRetryDelayMs = 250;

// Each retry, add this many MS to the delay.
// total delay is:
// (kLoaderPartialRetryDelayMs +
//  kAdditionalDelayPerRetryMs * (kMaxRetries - 1) / 2) * kMaxretries = 29250 ms
const int kAdditionalDelayPerRetryMs = 50;

// The number of milliseconds to wait before retrying when the server
// decides to not give us all the data at once.
const int kLoaderPartialRetryDelayMs = 25;

const int kHttpOK = 200;
const int kHttpPartialContent = 206;
const int kHttpRangeNotSatisfiable = 416;

ResourceMultiBufferDataProvider::ResourceMultiBufferDataProvider(
    UrlData* url_data,
    MultiBufferBlockId pos,
    bool is_client_audio_element,
    scoped_refptr<base::SingleThreadTaskRunner> task_runner)
    : pos_(pos),
      url_data_(url_data),
      retries_(0),
      cors_mode_(url_data->cors_mode()),
      original_url_(url_data->url()),
      is_client_audio_element_(is_client_audio_element),
      task_runner_(std::move(task_runner)) {
  DCHECK(url_data_) << " pos = " << pos;
  DCHECK_GE(pos, 0);
}

ResourceMultiBufferDataProvider::~ResourceMultiBufferDataProvider() {
  base::UmaHistogramCustomCounts(
      "Media.Network.TotalBytesReceived.SRC",
      base::saturated_cast<int>(total_bytes_received_), 1024, 1073741824, 100);
}

void ResourceMultiBufferDataProvider::Start() {
  DVLOG(1) << __func__ << " @ " << byte_pos();

  if (url_data_->length() > 0 && byte_pos() >= url_data_->length()) {
    task_runner_->PostTask(
        FROM_HERE, blink::BindOnce(&ResourceMultiBufferDataProvider::Terminate,
                                   weak_factory_.GetWeakPtr()));
    return;
  }

  // Prepare the request.
  WebURLRequest request(url_data_->url());
  request.SetRequestContext(is_client_audio_element_
                                ? mojom::blink::RequestContextType::AUDIO
                                : mojom::blink::RequestContextType::VIDEO);
  request.SetRequestDestination(
      is_client_audio_element_
          ? network::mojom::blink::RequestDestination::kAudio
          : network::mojom::blink::RequestDestination::kVideo);

  request.SetHttpHeaderField(
      WebString::FromUtf8(net::HttpRequestHeaders::kRange),
      WebString::FromUtf8(
          net::HttpByteRange::RightUnbounded(byte_pos()).GetHeaderValue()));

  // We would like to send an if-match header with the request to
  // tell the remote server that we really can't handle files other
  // than the one we already started playing. Unfortunately, doing
  // so will disable the http cache, and possibly other proxies
  // along the way. See crbug.com/41185060 and crbug.com/41300485 for more
  // information.

  if (url_data_->encoding_mode() ==
      media::DataSource::EncodingMode::kAllowGzip) {
    // Allow gzip for manifests or when explicitly requested.
    request.SetHttpHeaderField(
        WebString::FromUtf8(net::HttpRequestHeaders::kAcceptEncoding),
        WebString("gzip, identity;q=1, *;q=0"));
  } else {
    // Disable compression, compression for audio/video doesn't make sense...
    request.SetHttpHeaderField(
        WebString::FromUtf8(net::HttpRequestHeaders::kAcceptEncoding),
        WebString("identity;q=1, *;q=0"));
  }

  // Start resource loading.
  WebAssociatedURLLoaderOptions options;
  if (url_data_->cors_mode() != UrlData::CORS_UNSPECIFIED) {
    options.expose_all_response_headers = true;
    // The author header set is empty, no preflight should go ahead.
    options.preflight_policy =
        network::mojom::blink::CorsPreflightPolicy::kPreventPreflight;

    request.SetMode(network::mojom::blink::RequestMode::kCors);
    if (url_data_->cors_mode() != UrlData::CORS_USE_CREDENTIALS) {
      request.SetCredentialsMode(
          network::mojom::blink::CredentialsMode::kSameOrigin);
    }
  }

  if (auto url_index = url_data_->url_index()) {
    active_loader_ = url_index->fetch_context()->CreateUrlLoader(options);
    active_loader_->LoadAsynchronously(request, this);
  } else {
    url_data_->Fail();
  }
}

/////////////////////////////////////////////////////////////////////////////
// MultiBuffer::DataProvider implementation.
MultiBufferBlockId ResourceMultiBufferDataProvider::Tell() const {
  return pos_;
}

bool ResourceMultiBufferDataProvider::Available() const {
  if (fifo_.empty())
    return false;
  if (fifo_.back()->end_of_stream())
    return true;
  if (fifo_.front()->size() == static_cast<size_t>(block_size())) {
    return true;
  }
  return false;
}

int64_t ResourceMultiBufferDataProvider::AvailableBytes() const {
  int64_t bytes = 0;
  for (const auto& i : fifo_) {
    if (i->end_of_stream())
      break;
    bytes += i->size();
  }
  return bytes;
}

scoped_refptr<media::DataBuffer> ResourceMultiBufferDataProvider::Read() {
  DCHECK(Available());
  scoped_refptr<media::DataBuffer> ret = fifo_.front();
  fifo_.pop_front();
  ++pos_;
  return ret;
}

void ResourceMultiBufferDataProvider::SetDeferred(bool deferred) {
  if (active_loader_) {
    active_loader_->SetDefersLoading(deferred);
  }
}

/////////////////////////////////////////////////////////////////////////////
// WebAssociatedURLLoaderClient implementation.

bool ResourceMultiBufferDataProvider::WillFollowRedirect(
    const WebURL& new_url,
    const WebURLResponse& redirect_response) {
  DVLOG(1) << "willFollowRedirect";
  redirects_to_ = new_url;
  url_data_->set_valid_until(base::Time::Now() +
                             GetCacheValidUntil(redirect_response));

  // This test is vital for security!
  if (cors_mode_ == UrlData::CORS_UNSPECIFIED) {
    // We allow the redirect if the origin is the same.
    if (!SecurityOrigin::AreSameOrigin(original_url_, redirects_to_) ||
        !SecurityOrigin::AreSameOrigin(url_data_->url(), redirects_to_)) {
      // We also allow the redirect if we don't have any data in the
      // cache, as that means that no dangerous data mixing can occur.
      if (url_data_->multibuffer()->map().empty() && fifo_.empty())
        return true;

      active_loader_.reset();
      url_data_->Fail();
      return false;  // "this" may be deleted now.
    }
  }
  return true;
}

void ResourceMultiBufferDataProvider::DidSendData(
    uint64_t bytes_sent,
    uint64_t total_bytes_to_be_sent) {
  NOTIMPLEMENTED();
}

void ResourceMultiBufferDataProvider::DidReceiveResponse(
    const WebURLResponse& response) {
#if DCHECK_IS_ON()
  std::string version;
  switch (response.HttpVersion()) {
    case WebURLResponse::kHTTPVersion_0_9:
      version = "0.9";
      break;
    case WebURLResponse::kHTTPVersion_1_0:
      version = "1.0";
      break;
    case WebURLResponse::kHTTPVersion_1_1:
      version = "1.1";
      break;
    case WebURLResponse::kHTTPVersion_2_0:
      version = "2.1";
      break;
    case WebURLResponse::kHTTPVersionUnknown:
      version = "unknown";
      break;
  }
  DVLOG(1) << "didReceiveResponse: HTTP/" << version << " "
           << response.HttpStatusCode();
#endif
  DCHECK(active_loader_);

  if (!url_data_->url_index()) {
    url_data_->Fail();
    return;  // "this" may be deleted now.
  }

  scoped_refptr<UrlData> destination_url_data(url_data_.get());

  if (!redirects_to_.IsEmpty()) {
    destination_url_data = url_data_->url_index()->GetByUrl(
        redirects_to_, cors_mode_, url_data_->cache_lookup_mode());
    redirects_to_ = KURL();
  }

  base::Time last_modified;
  if (base::Time::FromString(
          response.HttpHeaderField("Last-Modified").Utf8().data(),
          &last_modified)) {
    destination_url_data->set_last_modified(last_modified);
  }

  destination_url_data->set_etag(
      response.HttpHeaderField("ETag").Utf8().data());

  destination_url_data->set_valid_until(base::Time::Now() +
                                        GetCacheValidUntil(response));

  bool cacheable = GetReasonsForUncacheability(response) == 0;
  destination_url_data->set_cacheable(cacheable);

  // Expected content length can be |kPositionNotSpecified|, in that case
  // |content_length_| is not specified and this is a streaming response.
  int64_t content_length = response.ExpectedContentLength();
  bool end_of_file = false;
  bool do_fail = false;
  // We get the response type here because aborting the loader may change it.
  const auto response_type = response.GetType();
  bytes_to_discard_ = 0;

  // We make a strong assumption that when we reach here we have either
  // received a response from HTTP/HTTPS protocol or the request was
  // successful (in particular range request). So we only verify the partial
  // response for HTTP and HTTPS protocol.
  if (destination_url_data->url().ProtocolIsInHttpFamily()) {
    bool partial_response = (response.HttpStatusCode() == kHttpPartialContent);
    bool ok_response = (response.HttpStatusCode() == kHttpOK);

    // Check to see whether the server supports byte ranges.
    std::string accept_ranges =
        response.HttpHeaderField("Accept-Ranges").Utf8();
    if (accept_ranges.contains("bytes")) {
      destination_url_data->set_range_supported();
    }

    // If we have verified the partial response and it is correct.
    // It's also possible for a server to support range requests
    // without advertising "Accept-Ranges: bytes".
    if (partial_response &&
        VerifyPartialResponse(response, destination_url_data)) {
      destination_url_data->set_range_supported();
    } else if (ok_response) {
      // We accept a 200 response for a Range:0- request, trusting the
      // Accept-Ranges header, because Apache thinks that's a reasonable thing
      // to return.
      destination_url_data->set_length(content_length);
      bytes_to_discard_ = byte_pos();
    } else if (response.HttpStatusCode() == kHttpRangeNotSatisfiable) {
      // Unsatisfiable range
      // Really, we should never request a range that doesn't exist, but
      // if we do, let's handle it in a sane way.
      // Note, we can't just call OnDataProviderEvent() here, because
      // url_data_ hasn't been updated to the final destination yet.
      end_of_file = true;
    } else {
      active_loader_.reset();
      // Can't call fail until readers have been migrated to the new
      // url data below.
      do_fail = true;
    }
  } else {
    if (content_length != kPositionNotSpecified) {
      destination_url_data->set_length(content_length + byte_pos());
    }
  }

  if (!do_fail) {
    destination_url_data =
        url_data_->url_index()->TryInsert(destination_url_data);
  }

  // This is vital for security!
  destination_url_data->set_is_cors_cross_origin(
      network::cors::IsCorsCrossOriginResponseType(response_type));

  // Only used for metrics.
  {
    WebString access_control =
        response.HttpHeaderField("Access-Control-Allow-Origin");
    if (!access_control.IsEmpty() && !access_control.Equals("null")) {
      // Note: When |access_control| is not *, we should verify that it matches
      // the requesting origin. Instead we just assume that it matches, which is
      // probably accurate enough for metrics.
      destination_url_data->set_has_access_control();
    }

    destination_url_data->set_mime_type(response.MimeType().Utf8());
  }

  destination_url_data->set_passed_timing_allow_origin_check(
      response.TimingAllowPassed());

  if (destination_url_data != url_data_.get()) {
    // At this point, we've encountered a redirect, or found a better url data
    // instance for the data that we're about to download.

    // First, let's take a ref on the current url data.
    scoped_refptr<UrlData> old_url_data(url_data_.get());
    destination_url_data->Use();

    // Take ownership of ourselves. (From the multibuffer)
    std::unique_ptr<DataProvider> self(
        url_data_->multibuffer()->RemoveProvider(this));
    url_data_ = destination_url_data.get();
    // Give the ownership to our new owner.
    url_data_->multibuffer()->AddProvider(std::move(self));

    // Call callback to let upstream users know about the transfer.
    // This will merge the data from the two multibuffers and
    // cause clients to start using the new UrlData.
    old_url_data->RedirectTo(destination_url_data);
  }

  if (do_fail) {
    destination_url_data->Fail();
    return;  // "this" may be deleted now.
  }

  // Get the response URL since it can differ from the request URL when a
  // service worker provided the response. Normally we would just use
  // ResponseUrl(), but ResourceMultiBufferDataProvider disallows mixing
  // constructed responses (new Response()) and native server responses, even if
  // they have the same response URL.
  KURL response_url;
  if (!response.WasFetchedViaServiceWorker() ||
      response.HasUrlListViaServiceWorker()) {
    response_url = response.ResponseUrl();
  }

  // This test is vital for security!
  if (!url_data_->ValidateDataOrigin(response_url)) {
    active_loader_.reset();
    url_data_->Fail();
    return;  // "this" may be deleted now.
  }

  if (end_of_file) {
    fifo_.push_back(media::DataBuffer::CreateEOSBuffer());
    url_data_->multibuffer()->OnDataProviderEvent(this);
  }
}

void ResourceMultiBufferDataProvider::DidReceiveData(
    base::span<const char> data) {
  DVLOG(1) << "didReceiveData: " << data.size() << " bytes";
  DCHECK(!Available());
  DCHECK(active_loader_);
  DCHECK_GT(data.size(), 0u);

  total_bytes_received_ += data.size();

  auto bytes_data = base::as_bytes(data);
  if (bytes_to_discard_) {
    const auto discard_length = std::min<size_t>(
        base::checked_cast<size_t>(bytes_to_discard_), bytes_data.size());
    bytes_data = bytes_data.subspan(discard_length);
    bytes_to_discard_ -= discard_length;
    if (bytes_data.empty()) {
      return;
    }
  }

  // When we receive data, we allow more retries.
  retries_ = 0;

  while (!bytes_data.empty()) {
    const size_t buffer_size = static_cast<size_t>(block_size());
    if (fifo_.empty() || fifo_.back()->size() == buffer_size) {
      fifo_.push_back(base::MakeRefCounted<media::DataBuffer>(buffer_size));
    }
    const size_t last_block_size = fifo_.back()->size();
    CHECK_GE(buffer_size, last_block_size);
    const size_t to_append =
        std::min(bytes_data.size(), buffer_size - last_block_size);
    CHECK_GT(to_append, 0u);

    fifo_.back()->Append(bytes_data.first(to_append));
    bytes_data = bytes_data.subspan(to_append);
  }

  url_data_->multibuffer()->OnDataProviderEvent(this);

  // Beware, this object might be deleted here.
}

void ResourceMultiBufferDataProvider::DidDownloadData(uint64_t dataLength) {
  NOTIMPLEMENTED();
}

void ResourceMultiBufferDataProvider::DidFinishLoading() {
  DVLOG(1) << "didFinishLoading";
  DCHECK(active_loader_.get());
  DCHECK(!Available());

  if (!url_data_->url_index()) {
    url_data_->Fail();
    return;  // "this" may be deleted now.
  }

  // We're done with the loader.
  active_loader_.reset();

  // If we didn't know the |instance_size_| we do now.
  int64_t size = byte_pos();

  // This request reports something smaller than what we've seen in the past,
  // Maybe it's transient error?
  if (url_data_->length() != kPositionNotSpecified &&
      size < url_data_->length()) {
    if (retries_ < kMaxRetries) {
      DVLOG(1) << " Partial data received.... @ pos = " << size;
      retries_++;
      task_runner_->PostDelayedTask(
          FROM_HERE,
          blink::BindOnce(&ResourceMultiBufferDataProvider::Start,
                          weak_factory_.GetWeakPtr()),
          base::Milliseconds(kLoaderPartialRetryDelayMs));
      return;
    } else {
      url_data_->Fail();
      return;  // "this" may be deleted now.
    }
  }

  url_data_->set_length(size);
  fifo_.push_back(media::DataBuffer::CreateEOSBuffer());
  url_data_->url_index()->TryInsert(url_data_.get());

  DCHECK(Available());
  url_data_->multibuffer()->OnDataProviderEvent(this);

  // Beware, this object might be deleted here.
}

void ResourceMultiBufferDataProvider::DidFail(const WebURLError& error) {
  DVLOG(1) << "didFail: reason=" << error.reason();
  DCHECK(active_loader_.get());
  active_loader_.reset();

  if (url_data_->url_index() && retries_ < kMaxRetries && pos_ != 0) {
    retries_++;
    task_runner_->PostDelayedTask(
        FROM_HERE,
        blink::BindOnce(&ResourceMultiBufferDataProvider::Start,
                        weak_factory_.GetWeakPtr()),
        base::Milliseconds(kLoaderFailedRetryDelayMs +
                           kAdditionalDelayPerRetryMs * retries_));
  } else {
    // We don't need to continue loading after failure.
    // Note that calling Fail() will most likely delete this object.
    url_data_->Fail();
  }
}

bool ResourceMultiBufferDataProvider::ParseContentRange(
    const std::string& content_range_str,
    int64_t* first_byte_position,
    int64_t* last_byte_position,
    int64_t* instance_size) {
  const char kUpThroughBytesUnit[] = "bytes ";
  if (!content_range_str.starts_with(kUpThroughBytesUnit)) {
    return false;
  }
  std::string range_spec =
      content_range_str.substr(sizeof(kUpThroughBytesUnit) - 1);
  size_t dash_offset = range_spec.find("-");
  size_t slash_offset = range_spec.find("/");

  if (dash_offset == std::string::npos || slash_offset == std::string::npos ||
      slash_offset < dash_offset || slash_offset + 1 == range_spec.length()) {
    return false;
  }
  if (!base::StringToInt64(range_spec.substr(0, dash_offset),
                           first_byte_position) ||
      !base::StringToInt64(
          range_spec.substr(dash_offset + 1, slash_offset - dash_offset - 1),
          last_byte_position)) {
    return false;
  }
  if (slash_offset == range_spec.length() - 2 &&
      range_spec[slash_offset + 1] == '*') {
    *instance_size = kPositionNotSpecified;
  } else {
    if (!base::StringToInt64(range_spec.substr(slash_offset + 1),
                             instance_size)) {
      return false;
    }
  }
  if (*last_byte_position < *first_byte_position ||
      (*instance_size != kPositionNotSpecified &&
       *last_byte_position >= *instance_size)) {
    return false;
  }

  return true;
}

void ResourceMultiBufferDataProvider::Terminate() {
  fifo_.push_back(media::DataBuffer::CreateEOSBuffer());
  url_data_->multibuffer()->OnDataProviderEvent(this);
}

int64_t ResourceMultiBufferDataProvider::byte_pos() const {
  int64_t ret = pos_;
  ret += fifo_.size();
  ret = ret << url_data_->multibuffer()->block_size_shift();
  if (!fifo_.empty()) {
    ret += fifo_.back()->size() - block_size();
  }
  return ret;
}

int64_t ResourceMultiBufferDataProvider::block_size() const {
  int64_t ret = 1;
  return ret << url_data_->multibuffer()->block_size_shift();
}

bool ResourceMultiBufferDataProvider::VerifyPartialResponse(
    const WebURLResponse& response,
    const scoped_refptr<UrlData>& url_data) {
  int64_t first_byte_position, last_byte_position, instance_size;
  if (!ParseContentRange(response.HttpHeaderField("Content-Range").Utf8(),
                         &first_byte_position, &last_byte_position,
                         &instance_size)) {
    return false;
  }

  if (url_data->length() == kPositionNotSpecified) {
    url_data->set_length(instance_size);
  }

  if (first_byte_position > byte_pos()) {
    return false;
  }
  if (last_byte_position + 1 < byte_pos()) {
    return false;
  }
  bytes_to_discard_ = byte_pos() - first_byte_position;

  return true;
}

}  // namespace blink
