blob: d5d12ca3e09002b39504aae16462719b9b396ad8 [file] [log] [blame]
// Copyright 2020 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 "services/network/web_bundle_url_loader_factory.h"
#include "base/metrics/histogram_functions.h"
#include "base/optional.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/web_package/web_bundle_parser.h"
#include "components/web_package/web_bundle_utils.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/cross_origin_read_blocking.h"
#include "services/network/web_bundle_chunked_buffer.h"
#include "services/network/web_bundle_memory_quota_consumer.h"
namespace network {
namespace {
constexpr size_t kBlockedBodyAllocationSize = 1;
void RecordLoadResult(
WebBundleURLLoaderFactory::SubresourceWebBundleLoadResult result) {
base::UmaHistogramEnumeration("SubresourceWebBundles.LoadResult", result);
}
void DeleteProducerAndRunCallback(
std::unique_ptr<mojo::DataPipeProducer> producer,
base::OnceCallback<void(MojoResult result)> callback,
MojoResult result) {
std::move(callback).Run(result);
}
// URLLoaderClient which wraps the real URLLoaderClient.
class WebBundleURLLoaderClient : public network::mojom::URLLoaderClient {
public:
WebBundleURLLoaderClient(
base::WeakPtr<WebBundleURLLoaderFactory> factory,
mojo::PendingRemote<network::mojom::URLLoaderClient> wrapped)
: factory_(factory), wrapped_(std::move(wrapped)) {}
private:
// network::mojom::URLLoaderClient implementation:
void OnReceiveResponse(
network::mojom::URLResponseHeadPtr response_head) override {
base::UmaHistogramCustomCounts(
"SubresourceWebBundles.ContentLength",
response_head->content_length < 0 ? 0 : response_head->content_length,
1, 50000000, 50);
wrapped_->OnReceiveResponse(std::move(response_head));
}
void OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head) override {
wrapped_->OnReceiveRedirect(redirect_info, std::move(response_head));
}
void OnUploadProgress(int64_t current_position,
int64_t total_size,
OnUploadProgressCallback ack_callback) override {
wrapped_->OnUploadProgress(current_position, total_size,
std::move(ack_callback));
}
void OnReceiveCachedMetadata(mojo_base::BigBuffer data) override {
wrapped_->OnReceiveCachedMetadata(std::move(data));
}
void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
wrapped_->OnTransferSizeUpdated(transfer_size_diff);
}
void OnStartLoadingResponseBody(
mojo::ScopedDataPipeConsumerHandle body) override {
if (factory_)
factory_->SetBundleStream(std::move(body));
// Send empty body to the wrapped URLLoaderClient.
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes = kBlockedBodyAllocationSize;
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
MojoResult result = mojo::CreateDataPipe(&options, &producer, &consumer);
if (result != MOJO_RESULT_OK) {
wrapped_->OnComplete(
URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
completed_ = true;
return;
}
wrapped_->OnStartLoadingResponseBody(std::move(consumer));
}
void OnComplete(const network::URLLoaderCompletionStatus& status) override {
if (completed_)
return;
wrapped_->OnComplete(status);
}
base::WeakPtr<WebBundleURLLoaderFactory> factory_;
mojo::Remote<network::mojom::URLLoaderClient> wrapped_;
bool completed_ = false;
};
} // namespace
class WebBundleURLLoaderFactory::URLLoader : public mojom::URLLoader {
public:
URLLoader(mojo::PendingReceiver<mojom::URLLoader> loader,
const ResourceRequest& request,
mojo::PendingRemote<mojom::URLLoaderClient> client,
const base::Optional<url::Origin>& request_initiator_origin_lock)
: url_(request.url),
request_mode_(request.mode),
request_initiator_(request.request_initiator),
request_initiator_origin_lock_(request_initiator_origin_lock),
receiver_(this, std::move(loader)),
client_(std::move(client)) {
receiver_.set_disconnect_handler(
base::BindOnce(&URLLoader::OnMojoDisconnect, GetWeakPtr()));
}
URLLoader(const URLLoader&) = delete;
URLLoader& operator=(const URLLoader&) = delete;
const GURL& url() const { return url_; }
const mojom::RequestMode& request_mode() const { return request_mode_; }
const base::Optional<url::Origin>& request_initiator() const {
return request_initiator_;
}
const base::Optional<url::Origin>& request_initiator_origin_lock() const {
return request_initiator_origin_lock_;
}
base::WeakPtr<URLLoader> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void OnResponse(mojom::URLResponseHeadPtr response) {
client_->OnReceiveResponse(std::move(response));
}
void OnData(mojo::ScopedDataPipeConsumerHandle consumer) {
client_->OnStartLoadingResponseBody(std::move(consumer));
}
void OnFail(net::Error error) {
client_->OnComplete(URLLoaderCompletionStatus(error));
delete this;
}
void OnWriteCompleted(MojoResult result) {
URLLoaderCompletionStatus status(
result == MOJO_RESULT_OK ? net::OK : net::ERR_INVALID_WEB_BUNDLE);
client_->OnComplete(status);
delete this;
}
void BlockResponseForCorb(mojom::URLResponseHeadPtr response_head) {
// A minimum implementation to block CORB-protected resources.
//
// TODO(crbug.com/1082020): Re-use
// network::URLLoader::BlockResponseForCorb(), instead of copying
// essential parts from there, so that the two implementations won't
// diverge further. That requires non-trivial refactoring.
CrossOriginReadBlocking::SanitizeBlockedResponse(response_head.get());
client_->OnReceiveResponse(std::move(response_head));
// Send empty body to the URLLoaderClient.
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
if (CreateDataPipe(nullptr, &producer, &consumer) != MOJO_RESULT_OK) {
OnFail(net::ERR_INSUFFICIENT_RESOURCES);
return;
}
producer.reset();
client_->OnStartLoadingResponseBody(std::move(consumer));
URLLoaderCompletionStatus status;
status.error_code = net::OK;
status.completion_time = base::TimeTicks::Now();
status.encoded_data_length = 0;
status.encoded_body_length = 0;
status.decoded_body_length = 0;
client_->OnComplete(status);
// Reset the connection to the URLLoaderClient. This helps ensure that we
// won't accidentally leak any data to the renderer from this point on.
client_.reset();
}
private:
// mojom::URLLoader
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const base::Optional<GURL>& new_url) override {
NOTREACHED();
}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {
// Not supported (do nothing).
}
void PauseReadingBodyFromNet() override {}
void ResumeReadingBodyFromNet() override {}
void OnMojoDisconnect() { delete this; }
const GURL url_;
mojom::RequestMode request_mode_;
base::Optional<url::Origin> request_initiator_;
// It is safe to hold |request_initiator_origin_lock_| in this factory because
// 1). |request_initiator_origin_lock| is a property of |URLLoaderFactory|
// (or, more accurately a property of |URLLoaderFactoryParams|), and
// 2) |WebURLLoader| is always associated with the same URLLoaderFactory
// (via URLLoaderFactory -> WebBundleManager -> WebBundleURLLoaderFactory
// -> WebBundleURLLoader).
const base::Optional<url::Origin> request_initiator_origin_lock_;
mojo::Receiver<mojom::URLLoader> receiver_;
mojo::Remote<mojom::URLLoaderClient> client_;
base::WeakPtrFactory<URLLoader> weak_ptr_factory_{this};
};
class WebBundleURLLoaderFactory::BundleDataSource
: public web_package::mojom::BundleDataSource,
public mojo::DataPipeDrainer::Client {
public:
using ReadToDataPipeCallback = base::OnceCallback<void(MojoResult result)>;
BundleDataSource(mojo::PendingReceiver<web_package::mojom::BundleDataSource>
data_source_receiver,
mojo::ScopedDataPipeConsumerHandle bundle_body,
std::unique_ptr<WebBundleMemoryQuotaConsumer>
web_bundle_memory_quota_consumer,
base::OnceClosure memory_quota_exceeded_closure,
base::OnceClosure data_completed_closure)
: data_source_receiver_(this, std::move(data_source_receiver)),
pipe_drainer_(
std::make_unique<mojo::DataPipeDrainer>(this,
std::move(bundle_body))),
web_bundle_memory_quota_consumer_(
std::move(web_bundle_memory_quota_consumer)),
memory_quota_exceeded_closure_(
std::move(memory_quota_exceeded_closure)),
data_completed_closure_(std::move(data_completed_closure)) {}
~BundleDataSource() override {
// The receiver must be closed before destructing pending callbacks in
// |pending_reads_| / |pending_reads_to_data_pipe_|.
data_source_receiver_.reset();
}
BundleDataSource(const BundleDataSource&) = delete;
BundleDataSource& operator=(const BundleDataSource&) = delete;
void ReadToDataPipe(mojo::ScopedDataPipeProducerHandle producer,
uint64_t offset,
uint64_t length,
ReadToDataPipeCallback callback) {
TRACE_EVENT0("loading", "BundleDataSource::ReadToDataPipe");
if (!finished_loading_ && !buffer_.ContainsAll(offset, length)) {
// Current implementation does not support progressive loading of inner
// response body.
PendingReadToDataPipe pending;
pending.producer = std::move(producer);
pending.offset = offset;
pending.length = length;
pending.callback = std::move(callback);
pending_reads_to_data_pipe_.push_back(std::move(pending));
return;
}
auto data_source = buffer_.CreateDataSource(offset, length);
if (!data_source) {
// When there is no body to send, returns OK here without creating a
// DataPipeProducer.
std::move(callback).Run(MOJO_RESULT_OK);
return;
}
auto writer = std::make_unique<mojo::DataPipeProducer>(std::move(producer));
mojo::DataPipeProducer* raw_writer = writer.get();
raw_writer->Write(std::move(data_source),
base::BindOnce(&DeleteProducerAndRunCallback,
std::move(writer), std::move(callback)));
}
// mojom::BundleDataSource
void Read(uint64_t offset, uint64_t length, ReadCallback callback) override {
TRACE_EVENT0("loading", "BundleDataSource::Read");
if (!finished_loading_ && !buffer_.ContainsAll(offset, length)) {
PendingRead pending;
pending.offset = offset;
pending.length = length;
pending.callback = std::move(callback);
pending_reads_.push_back(std::move(pending));
return;
}
uint64_t out_len = buffer_.GetAvailableLength(offset, length);
std::vector<uint8_t> output(base::checked_cast<size_t>(out_len));
buffer_.ReadData(offset, out_len, output.data());
std::move(callback).Run(std::move(output));
}
// mojo::DataPipeDrainer::Client
void OnDataAvailable(const void* data, size_t num_bytes) override {
DCHECK(!finished_loading_);
if (!web_bundle_memory_quota_consumer_->AllocateMemory(num_bytes)) {
AbortPendingReads();
if (memory_quota_exceeded_closure_) {
// Defer calling |memory_quota_exceeded_closure_| to avoid the
// UAF call in DataPipeDrainer::ReadData().
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, std::move(memory_quota_exceeded_closure_));
}
return;
}
buffer_.Append(reinterpret_cast<const uint8_t*>(data), num_bytes);
ProcessPendingReads();
}
void OnDataComplete() override {
DCHECK(!finished_loading_);
base::UmaHistogramCustomCounts(
"SubresourceWebBundles.ReceivedSize",
base::saturated_cast<base::Histogram::Sample>(buffer_.size()), 1,
50000000, 50);
DCHECK(data_completed_closure_);
// Defer calling |data_completed_closure_| not to run
// |data_completed_closure_| before |memory_quota_exceeded_closure_|.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, std::move(data_completed_closure_));
finished_loading_ = true;
ProcessPendingReads();
}
private:
void ProcessPendingReads() {
std::vector<PendingRead> pendings(std::move(pending_reads_));
std::vector<PendingReadToDataPipe> pipe_pendings(
std::move(pending_reads_to_data_pipe_));
for (auto& pending : pendings) {
Read(pending.offset, pending.length, std::move(pending.callback));
}
for (auto& pending : pipe_pendings) {
ReadToDataPipe(std::move(pending.producer), pending.offset,
pending.length, std::move(pending.callback));
}
}
void AbortPendingReads() {
std::vector<PendingRead> pendings(std::move(pending_reads_));
std::vector<PendingReadToDataPipe> pipe_pendings(
std::move(pending_reads_to_data_pipe_));
for (auto& pending : pendings) {
std::move(pending.callback).Run(std::vector<uint8_t>());
}
for (auto& pending : pipe_pendings) {
std::move(pending.callback).Run(MOJO_RESULT_NOT_FOUND);
}
}
struct PendingRead {
uint64_t offset;
uint64_t length;
ReadCallback callback;
};
struct PendingReadToDataPipe {
mojo::ScopedDataPipeProducerHandle producer;
uint64_t offset;
uint64_t length;
ReadToDataPipeCallback callback;
};
mojo::Receiver<web_package::mojom::BundleDataSource> data_source_receiver_;
WebBundleChunkedBuffer buffer_;
std::vector<PendingRead> pending_reads_;
std::vector<PendingReadToDataPipe> pending_reads_to_data_pipe_;
bool finished_loading_ = false;
std::unique_ptr<mojo::DataPipeDrainer> pipe_drainer_;
std::unique_ptr<WebBundleMemoryQuotaConsumer>
web_bundle_memory_quota_consumer_;
base::OnceClosure memory_quota_exceeded_closure_;
base::OnceClosure data_completed_closure_;
};
WebBundleURLLoaderFactory::WebBundleURLLoaderFactory(
const GURL& bundle_url,
mojo::Remote<mojom::WebBundleHandle> web_bundle_handle,
const base::Optional<url::Origin>& request_initiator_origin_lock,
std::unique_ptr<WebBundleMemoryQuotaConsumer>
web_bundle_memory_quota_consumer)
: bundle_url_(bundle_url),
web_bundle_handle_(std::move(web_bundle_handle)),
request_initiator_origin_lock_(request_initiator_origin_lock),
web_bundle_memory_quota_consumer_(
std::move(web_bundle_memory_quota_consumer)) {}
WebBundleURLLoaderFactory::~WebBundleURLLoaderFactory() {
for (auto loader : pending_loaders_) {
if (loader)
loader->OnFail(net::ERR_FAILED);
}
}
base::WeakPtr<WebBundleURLLoaderFactory> WebBundleURLLoaderFactory::GetWeakPtr()
const {
return weak_ptr_factory_.GetWeakPtr();
}
void WebBundleURLLoaderFactory::SetBundleStream(
mojo::ScopedDataPipeConsumerHandle body) {
mojo::PendingRemote<web_package::mojom::BundleDataSource> data_source;
source_ = std::make_unique<BundleDataSource>(
data_source.InitWithNewPipeAndPassReceiver(), std::move(body),
std::move(web_bundle_memory_quota_consumer_),
base::BindOnce(&WebBundleURLLoaderFactory::OnMemoryQuotaExceeded,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&WebBundleURLLoaderFactory::OnDataCompleted,
weak_ptr_factory_.GetWeakPtr()));
// WebBundleParser will self-destruct on remote mojo ends' disconnection.
new web_package::WebBundleParser(parser_.BindNewPipeAndPassReceiver(),
std::move(data_source));
parser_->ParseMetadata(
base::BindOnce(&WebBundleURLLoaderFactory::OnMetadataParsed,
weak_ptr_factory_.GetWeakPtr()));
}
mojo::PendingRemote<mojom::URLLoaderClient>
WebBundleURLLoaderFactory::WrapURLLoaderClient(
mojo::PendingRemote<mojom::URLLoaderClient> wrapped) {
mojo::PendingRemote<mojom::URLLoaderClient> client;
auto client_impl = std::make_unique<WebBundleURLLoaderClient>(
weak_ptr_factory_.GetWeakPtr(), std::move(wrapped));
mojo::MakeSelfOwnedReceiver(std::move(client_impl),
client.InitWithNewPipeAndPassReceiver());
return client;
}
void WebBundleURLLoaderFactory::StartSubresourceRequest(
mojo::PendingReceiver<mojom::URLLoader> receiver,
const ResourceRequest& url_request,
mojo::PendingRemote<mojom::URLLoaderClient> client) {
TRACE_EVENT0("loading", "WebBundleURLLoaderFactory::StartSubresourceRequest");
URLLoader* loader =
new URLLoader(std::move(receiver), url_request, std::move(client),
request_initiator_origin_lock_);
if (metadata_error_) {
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
return;
}
if (quota_exceeded_error_) {
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
return;
}
if (!metadata_) {
pending_loaders_.push_back(loader->GetWeakPtr());
return;
}
StartLoad(loader);
}
void WebBundleURLLoaderFactory::StartLoad(URLLoader* loader) {
DCHECK(metadata_);
if (!loader)
return;
auto it = metadata_->requests.find(loader->url());
if (it == metadata_->requests.end()) {
web_bundle_handle_->OnWebBundleError(
mojom::WebBundleErrorType::kResourceNotFound,
loader->url().possibly_invalid_spec() +
" is not found in the WebBundle.");
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
return;
}
// Currently, we just return the first response for the URL.
// TODO(crbug.com/1082020): Support variant matching.
auto& location = it->second->response_locations[0];
parser_->ParseResponse(
location->offset, location->length,
base::BindOnce(&WebBundleURLLoaderFactory::OnResponseParsed,
weak_ptr_factory_.GetWeakPtr(), loader->GetWeakPtr()));
}
void WebBundleURLLoaderFactory::OnMetadataParsed(
web_package::mojom::BundleMetadataPtr metadata,
web_package::mojom::BundleMetadataParseErrorPtr error) {
TRACE_EVENT0("loading", "WebBundleURLLoaderFactory::OnMetadataParsed");
if (error) {
metadata_error_ = std::move(error);
MaybeRecordLoadResult();
web_bundle_handle_->OnWebBundleError(
mojom::WebBundleErrorType::kMetadataParseError,
metadata_error_->message);
auto pending_loaders = std::move(pending_loaders_);
for (auto loader : pending_loaders) {
if (loader)
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
}
source_.reset();
parser_.reset();
return;
}
metadata_ = std::move(metadata);
MaybeRecordLoadResult();
for (auto loader : pending_loaders_)
StartLoad(loader.get());
pending_loaders_.clear();
}
void WebBundleURLLoaderFactory::OnResponseParsed(
base::WeakPtr<URLLoader> loader,
web_package::mojom::BundleResponsePtr response,
web_package::mojom::BundleResponseParseErrorPtr error) {
TRACE_EVENT0("loading", "WebBundleURLLoaderFactory::OnResponseParsed");
if (!loader)
return;
if (error) {
web_bundle_handle_->OnWebBundleError(
mojom::WebBundleErrorType::kResponseParseError, error->message);
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
return;
}
// Currently we allow only net::HTTP_OK responses in bundles.
// TODO(crbug.com/990733): Revisit this once
// https://github.com/WICG/webpackage/issues/478 is resolved.
if (response->response_code != net::HTTP_OK) {
web_bundle_handle_->OnWebBundleError(
mojom::WebBundleErrorType::kResponseParseError,
"Invalid response code " +
base::NumberToString(response->response_code));
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
return;
}
mojom::URLResponseHeadPtr response_head =
web_package::CreateResourceResponse(response);
response_head->web_bundle_url = bundle_url_;
// Add an artifical "X-Content-Type-Options: "nosniff" header, which is
// explained at
// https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html#name-responses.
response_head->headers->SetHeader("X-Content-Type-Options", "nosniff");
auto corb_analyzer =
std::make_unique<CrossOriginReadBlocking::ResponseAnalyzer>(
loader->url(), loader->request_initiator(), *response_head,
loader->request_initiator_origin_lock(), loader->request_mode());
if (corb_analyzer->ShouldBlock()) {
loader->BlockResponseForCorb(std::move(response_head));
return;
}
loader->OnResponse(std::move(response_head));
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
if (CreateDataPipe(nullptr, &producer, &consumer) != MOJO_RESULT_OK) {
loader->OnFail(net::ERR_INSUFFICIENT_RESOURCES);
return;
}
loader->OnData(std::move(consumer));
source_->ReadToDataPipe(
std::move(producer), response->payload_offset, response->payload_length,
base::BindOnce(&URLLoader::OnWriteCompleted, loader->GetWeakPtr()));
}
void WebBundleURLLoaderFactory::OnMemoryQuotaExceeded() {
TRACE_EVENT0("loading", "WebBundleURLLoaderFactory::OnMemoryQuotaExceeded");
quota_exceeded_error_ = true;
MaybeRecordLoadResult();
web_bundle_handle_->OnWebBundleError(
mojom::WebBundleErrorType::kMemoryQuotaExceeded,
"Memory quota exceeded. Currently, there is an upper limit on the total "
"size of subresource web bundles in a process. See "
"https://crbug.com/1154140 for more details.");
auto pending_loaders = std::move(pending_loaders_);
for (auto loader : pending_loaders) {
if (loader) {
loader->OnFail(net::ERR_INVALID_WEB_BUNDLE);
}
}
source_.reset();
parser_.reset();
}
void WebBundleURLLoaderFactory::OnDataCompleted() {
data_completed_ = true;
MaybeRecordLoadResult();
}
void WebBundleURLLoaderFactory::MaybeRecordLoadResult() {
if (load_result_recorded_)
return;
if (quota_exceeded_error_) {
RecordLoadResult(SubresourceWebBundleLoadResult::kMemoryQuotaExceeded);
load_result_recorded_ = true;
return;
}
if (metadata_error_) {
RecordLoadResult(SubresourceWebBundleLoadResult::kMetadataParseError);
load_result_recorded_ = true;
return;
}
if (metadata_ && data_completed_) {
RecordLoadResult(SubresourceWebBundleLoadResult::kSuccess);
load_result_recorded_ = true;
return;
}
}
} // namespace network