diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc b/chromium/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc new file mode 100644 index 00000000000..27c045a7fa4 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc @@ -0,0 +1,576 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.h" + +#include <stddef.h> +#include <utility> + +#include "base/bind.h" +#include "base/bits.h" +#include "base/callback_helpers.h" +#include "base/location.h" +#include "base/metrics/histogram_macros.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "net/http/http_byte_range.h" +#include "net/http/http_request_headers.h" +#include "services/network/public/cpp/cors/cors.h" +#include "third_party/blink/public/common/loader/previews_state.h" +#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h" +#include "third_party/blink/public/platform/media/resource_fetch_context.h" +#include "third_party/blink/public/platform/media/url_index.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" + +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()), + origin_(url_data->url().GetOrigin()), + 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() = default; + +void ResourceMultiBufferDataProvider::Start() { + DVLOG(1) << __func__ << " @ " << byte_pos(); + if (url_data_->length() > 0 && byte_pos() >= url_data_->length()) { + task_runner_->PostTask( + FROM_HERE, base::BindOnce(&ResourceMultiBufferDataProvider::Terminate, + weak_factory_.GetWeakPtr())); + return; + } + + // Prepare the request. + WebURLRequest request(url_data_->url()); + request.SetRequestContext(is_client_audio_element_ + ? mojom::RequestContextType::AUDIO + : mojom::RequestContextType::VIDEO); + request.SetRequestDestination( + is_client_audio_element_ ? network::mojom::RequestDestination::kAudio + : network::mojom::RequestDestination::kVideo); + request.SetHttpHeaderField( + WebString::FromUTF8(net::HttpRequestHeaders::kRange), + WebString::FromUTF8( + net::HttpByteRange::RightUnbounded(byte_pos()).GetHeaderValue())); + + if (url_data_->length() == kPositionNotSpecified && + url_data_->CachedSize() == 0 && url_data_->BytesReadFromCache() == 0 && + WebNetworkStateNotifier::SaveDataEnabled() && + (url_data_->url().SchemeIs(url::kHttpScheme) || + url_data_->url().SchemeIs(url::kHttpsScheme))) { + // This lets the data reduction proxy know that we don't have any previously + // cached data for this resource. We can only send it if this is the first + // request for this resource. + request.SetPreviewsState(request.GetPreviewsState() | + PreviewsTypes::kSrcVideoRedirectOn); + } + + // 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/504194 and crbug/689989 for more information. + + // Disable compression, compression for audio/video doesn't make sense... + request.SetHttpHeaderField( + WebString::FromUTF8(net::HttpRequestHeaders::kAcceptEncoding), + WebString::FromUTF8("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::CorsPreflightPolicy::kPreventPreflight; + + request.SetMode(network::mojom::RequestMode::kCors); + if (url_data_->cors_mode() != UrlData::CORS_USE_CREDENTIALS) { + request.SetCredentialsMode(network::mojom::CredentialsMode::kSameOrigin); + } + } + + active_loader_ = + url_data_->url_index()->fetch_context()->CreateUrlLoader(options); + active_loader_->LoadAsynchronously(request, this); +} + +///////////////////////////////////////////////////////////////////////////// +// 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()->data_size() == 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->data_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 (origin_ != redirects_to_.GetOrigin()) { + // 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_); + + scoped_refptr<UrlData> destination_url_data(url_data_); + + if (!redirects_to_.is_empty()) { + destination_url_data = url_data_->url_index()->GetByUrl( + redirects_to_, cors_mode_, UrlIndex::kNormal); + redirects_to_ = GURL(); + } + + 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)); + + destination_url_data->set_cacheable(GetReasonsForUncacheability(response) == + 0); + + // 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().SchemeIsHTTPOrHTTPS()) { + 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.find("bytes") != std::string::npos) + 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 { + destination_url_data->set_range_supported(); + 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(); + } + } + + if (destination_url_data != url_data_) { + // 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_); + 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. + GURL response_url; + if (!response.WasFetchedViaServiceWorker() || + response.HasUrlListViaServiceWorker()) { + response_url = response.ResponseUrl(); + } + + // This test is vital for security! + if (!url_data_->ValidateDataOrigin(response_url.GetOrigin())) { + 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(const char* data, + int data_length) { + DVLOG(1) << "didReceiveData: " << data_length << " bytes"; + DCHECK(!Available()); + DCHECK(active_loader_); + DCHECK_GT(data_length, 0); + + if (bytes_to_discard_) { + uint64_t tmp = std::min<uint64_t>(bytes_to_discard_, data_length); + data_length -= tmp; + data += tmp; + bytes_to_discard_ -= tmp; + if (data_length == 0) + return; + } + + // When we receive data, we allow more retries. + retries_ = 0; + + while (data_length) { + if (fifo_.empty() || fifo_.back()->data_size() == block_size()) { + fifo_.push_back(new media::DataBuffer(block_size())); + fifo_.back()->set_data_size(0); + } + int last_block_size = fifo_.back()->data_size(); + int to_append = std::min<int>(data_length, block_size() - last_block_size); + DCHECK_GT(to_append, 0); + memcpy(fifo_.back()->writable_data() + last_block_size, data, to_append); + data += to_append; + fifo_.back()->set_data_size(last_block_size + to_append); + data_length -= 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()); + + // 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, + base::BindOnce(&ResourceMultiBufferDataProvider::Start, + weak_factory_.GetWeakPtr()), + base::TimeDelta::FromMilliseconds(kLoaderPartialRetryDelayMs)); + return; + } else { + url_data_->Fail(); + return; // "this" may be deleted now. + } + } + + url_data_->set_length(size); + fifo_.push_back(media::DataBuffer::CreateEOSBuffer()); + + if (url_data_->url_index()) { + url_data_->url_index()->TryInsert(url_data_); + } + + 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 (retries_ < kMaxRetries && pos_ != 0) { + retries_++; + task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&ResourceMultiBufferDataProvider::Start, + weak_factory_.GetWeakPtr()), + base::TimeDelta::FromMilliseconds( + 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 (!base::StartsWith(content_range_str, kUpThroughBytesUnit, + base::CompareCase::SENSITIVE)) { + 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()->data_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 |