// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/android/content_url_loader_factory.h"
#include <limits>
#include <string>
#include <vector>
#include "base/android/content_uri_utils.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/file_url_loader.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/file_data_source.h"
#include "net/base/net_errors.h"
#include "net/http/http_byte_range.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
// TODO(eroman): Add unit-tests for "X-Chrome-intent-type"
// (see url_request_content_job_unittest.cc).
// TODO(eroman): Remove duplication with file_url_loader_factory.cc (notably
// Range header parsing).
namespace content {
namespace {
constexpr size_t kDefaultContentUrlPipeSize = 65536;
// Assigns the byte range that has been requested based on the Range header.
// This assumes the simplest form of the Range header using a single range.
// If no byte range was specified, the output range will cover the entire file.
bool GetRequestedByteRange(const network::ResourceRequest& request,
uint64_t content_size,
uint64_t* first_byte_to_send,
uint64_t* total_bytes_to_send) {
*first_byte_to_send = 0;
*total_bytes_to_send = content_size;
std::optional<std::string> range_header =
request.headers.GetHeader(net::HttpRequestHeaders::kRange);
std::vector<net::HttpByteRange> ranges;
if (!range_header ||
!net::HttpUtil::ParseRangeHeader(*range_header, &ranges)) {
return true;
}
// Only handle a simple Range header for a single range.
if (ranges.size() != 1 || !ranges[0].IsValid() ||
!ranges[0].ComputeBounds(content_size)) {
return false;
}
net::HttpByteRange byte_range = ranges[0];
*first_byte_to_send = byte_range.first_byte_position();
*total_bytes_to_send =
byte_range.last_byte_position() - *first_byte_to_send + 1;
return true;
}
// Gets the mimetype for |content_path| either by asking the content provider,
// or by using the special Chrome request header X-Chrome-intent-type.
void GetMimeType(const network::ResourceRequest& request,
const base::FilePath& content_path,
std::string* out_mime_type) {
out_mime_type->clear();
if (request.resource_type ==
static_cast<int>(blink::mojom::ResourceType::kMainFrame)) {
std::optional<std::string> intent_type_header =
request.headers.GetHeader("X-Chrome-intent-type");
if (intent_type_header) {
*out_mime_type = std::move(intent_type_header).value();
}
}
if (out_mime_type->empty())
*out_mime_type = base::GetContentUriMimeType(content_path);
}
class ContentURLLoader : public network::mojom::URLLoader {
public:
static void CreateAndStart(
const network::ResourceRequest& request,
mojo::PendingReceiver<network::mojom::URLLoader> loader,
mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote) {
// Owns itself. Will live as long as its URLLoader and URLLoaderClient
// bindings are alive - essentially until either the client gives up or all
// file data has been sent to it.
auto* content_url_loader = new ContentURLLoader;
content_url_loader->Start(request, std::move(loader),
std::move(client_remote));
}
ContentURLLoader(const ContentURLLoader&) = delete;
ContentURLLoader& operator=(const ContentURLLoader&) = delete;
// network::mojom::URLLoader:
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const std::optional<GURL>& new_url) override {}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {}
void PauseReadingBodyFromNet() override {}
void ResumeReadingBodyFromNet() override {}
private:
ContentURLLoader() = default;
~ContentURLLoader() override = default;
void Start(
const network::ResourceRequest& request,
mojo::PendingReceiver<network::mojom::URLLoader> loader,
mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote) {
bool disable_web_security =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableWebSecurity);
network::mojom::FetchResponseType response_type =
network::cors::CalculateResponseType(request.mode,
disable_web_security);
// Don't allow content:// requests with kSameOrigin or kCors* unless the
// web security is turned off.
if ((!disable_web_security &&
request.mode == network::mojom::RequestMode::kSameOrigin) ||
response_type == network::mojom::FetchResponseType::kCors) {
mojo::Remote<network::mojom::URLLoaderClient>(std::move(client_remote))
->OnComplete(
network::URLLoaderCompletionStatus(network::CorsErrorStatus(
network::mojom::CorsError::kCorsDisabledScheme)));
return;
}
auto head = network::mojom::URLResponseHead::New();
head->request_start = head->response_start = base::TimeTicks::Now();
head->response_type = response_type;
receiver_.Bind(std::move(loader));
receiver_.set_disconnect_handler(base::BindOnce(
&ContentURLLoader::OnMojoDisconnect, base::Unretained(this)));
mojo::Remote<network::mojom::URLLoaderClient> client(
std::move(client_remote));
DCHECK(request.url.SchemeIs("content"));
base::FilePath path = base::FilePath(request.url.spec());
// Get the file length.
base::File::Info info;
if (!base::GetFileInfo(path, &info))
return CompleteWithFailure(std::move(client), net::ERR_FILE_NOT_FOUND);
uint64_t first_byte_to_send;
uint64_t total_bytes_to_send;
if (!GetRequestedByteRange(request, (info.size > 0) ? info.size : 0,
&first_byte_to_send, &total_bytes_to_send) ||
(std::numeric_limits<int64_t>::max() < first_byte_to_send) ||
(std::numeric_limits<int64_t>::max() < total_bytes_to_send)) {
return CompleteWithFailure(std::move(client),
net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
}
mojo::ScopedDataPipeProducerHandle producer_handle;
mojo::ScopedDataPipeConsumerHandle consumer_handle;
if (mojo::CreateDataPipe(kDefaultContentUrlPipeSize, producer_handle,
consumer_handle) != MOJO_RESULT_OK) {
return CompleteWithFailure(std::move(client), net::ERR_FAILED);
}
base::File file = base::OpenContentUri(
path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
return CompleteWithFailure(
std::move(client), net::FileErrorToNetError(file.error_details()));
}
head->content_length = total_bytes_to_send;
total_bytes_written_ = total_bytes_to_send;
// Set the mimetype of the response.
GetMimeType(request, path, &head->mime_type);
if (!head->mime_type.empty()) {
head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
head->headers->SetHeader(net::HttpRequestHeaders::kContentType,
head->mime_type);
}
client->OnReceiveResponse(std::move(head), std::move(consumer_handle),
std::nullopt);
client_ = std::move(client);
if (total_bytes_to_send == 0) {
// There's no more data, so we're already done.
OnFileWritten(MOJO_RESULT_OK);
return;
}
// In case of a range request, seek to the appropriate position before
// sending the remaining bytes asynchronously. Under normal conditions
// (i.e., no range request) this Seek is effectively a no-op.
auto data_source = std::make_unique<mojo::FileDataSource>(std::move(file));
data_source->SetRange(first_byte_to_send,
first_byte_to_send + total_bytes_to_send);
data_producer_ =
std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle));
data_producer_->Write(std::move(data_source),
base::BindOnce(&ContentURLLoader::OnFileWritten,
base::Unretained(this)));
}
void CompleteWithFailure(mojo::Remote<network::mojom::URLLoaderClient> client,
net::Error net_error) {
client->OnComplete(network::URLLoaderCompletionStatus(net_error));
MaybeDeleteSelf();
}
void OnMojoDisconnect() {
receiver_.reset();
MaybeDeleteSelf();
}
void MaybeDeleteSelf() {
if (!receiver_.is_bound() && !client_.is_bound())
delete this;
}
void OnFileWritten(MojoResult result) {
// All the data has been written now. Close the data pipe. The consumer will
// be notified that there will be no more data to read from now.
data_producer_.reset();
if (result == MOJO_RESULT_OK) {
network::URLLoaderCompletionStatus status(net::OK);
status.encoded_data_length = total_bytes_written_;
status.encoded_body_length = total_bytes_written_;
status.decoded_body_length = total_bytes_written_;
client_->OnComplete(status);
} else {
client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
}
client_.reset();
MaybeDeleteSelf();
}
std::unique_ptr<mojo::DataPipeProducer> data_producer_;
mojo::Receiver<network::mojom::URLLoader> receiver_{this};
mojo::Remote<network::mojom::URLLoaderClient> client_;
// In case of successful loads, this holds the total of bytes written.
// It is used to set some of the URLLoaderCompletionStatus data passed back
// to the URLLoaderClients (eg SimpleURLLoader).
size_t total_bytes_written_ = 0;
};
} // namespace
ContentURLLoaderFactory::ContentURLLoaderFactory(
scoped_refptr<base::SequencedTaskRunner> task_runner,
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver)
: network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)),
task_runner_(std::move(task_runner)) {}
ContentURLLoaderFactory::~ContentURLLoaderFactory() = default;
void ContentURLLoaderFactory::CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ContentURLLoader::CreateAndStart, request,
std::move(loader), std::move(client)));
}
// static
mojo::PendingRemote<network::mojom::URLLoaderFactory>
ContentURLLoaderFactory::Create() {
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
// The ContentURLLoaderFactory will delete itself when there are no more
// receivers - see the network::SelfDeletingURLLoaderFactory::OnDisconnect
// method.
new ContentURLLoaderFactory(
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}),
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
} // namespace content