blob: e88a03bf48d5ef878c4ede2b4623c0dea7b90a24 [file] [log] [blame]
// Copyright 2019 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 "fuchsia/engine/browser/content_directory_loader_factory.h"
#include <lib/fdio/directory.h>
#include <lib/fdio/fdio.h>
#include <map>
#include <memory>
#include <utility>
#include "base/files/memory_mapped_file.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/json/json_reader.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "fuchsia/engine/common/web_engine_content_client.h"
#include "fuchsia/engine/switches.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "net/base/filename_util.h"
#include "net/base/mime_sniffer.h"
#include "net/base/parse_number.h"
#include "services/network/public/cpp/resource_response.h"
#include "services/network/public/mojom/url_loader.mojom.h"
namespace {
using ContentDirectoriesMap =
std::map<std::string, fidl::InterfaceHandle<fuchsia::io::Directory>>;
ContentDirectoriesMap ParseContentDirectoriesFromCommandLine() {
ContentDirectoriesMap directories;
// Parse the list of content directories from the command line.
std::string content_directories_unsplit =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kContentDirectories);
if (content_directories_unsplit.empty())
return {};
base::StringPairs named_handle_ids;
if (!base::SplitStringIntoKeyValuePairs(content_directories_unsplit, '=', ',',
&named_handle_ids)) {
LOG(WARNING) << "Couldn't parse --" << switches::kContentDirectories
<< " into KV pairs: " << content_directories_unsplit;
return {};
}
for (const auto& named_handle_id : named_handle_ids) {
uint32_t handle_id = 0;
if (!net::ParseUint32(named_handle_id.second, &handle_id)) {
DLOG(FATAL) << "Couldn't parse handle ID as uint32: "
<< named_handle_id.second;
continue;
}
auto directory_channel = zx::channel(zx_take_startup_handle(handle_id));
if (directory_channel == ZX_HANDLE_INVALID) {
DLOG(FATAL) << "Couldn't take startup handle: " << handle_id;
continue;
}
directories.emplace(named_handle_id.first,
fidl::InterfaceHandle<fuchsia::io::Directory>(
std::move(directory_channel)));
}
return directories;
}
// Gets the process-global list of content directory channels.
ContentDirectoriesMap* GetContentDirectories() {
static base::NoDestructor<ContentDirectoriesMap> directories(
ParseContentDirectoriesFromCommandLine());
return directories.get();
}
// Returns a list of populated response HTTP headers.
// |mime_type|: The MIME type of the resource.
// |charset|: The resource's character set. Optional. If omitted, the browser
// will assume the charset to be "text/plain" by default.
scoped_refptr<net::HttpResponseHeaders> CreateHeaders(
base::StringPiece mime_type,
const base::Optional<std::string>& charset) {
constexpr char kXFrameOptionsHeader[] = "X-Frame-Options: DENY";
constexpr char kCacheHeader[] = "Cache-Control: no-cache";
constexpr char kContentTypePrefix[] = "Content-Type: ";
constexpr char kCharsetSeparator[] = "; charset=";
auto headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\r\n");
headers->AddHeader(kXFrameOptionsHeader);
headers->AddHeader(kCacheHeader);
if (charset) {
headers->AddHeader(base::StrCat(
{kContentTypePrefix, mime_type, kCharsetSeparator, *charset}));
} else {
headers->AddHeader(base::StrCat({kContentTypePrefix, mime_type}));
}
return headers;
}
// Copies data from a fuchsia.io.Node file into a URL response stream.
class ContentDirectoryURLLoader : public network::mojom::URLLoader {
public:
ContentDirectoryURLLoader() = default;
~ContentDirectoryURLLoader() final = default;
// Creates a read-only MemoryMappedFile view to |file|.
bool MapFile(fidl::InterfaceHandle<fuchsia::io::Node> file,
base::MemoryMappedFile* mmap) {
// Bind the file channel to a FDIO entry and then a file descriptor so that
// we can use it for reading.
fdio_t* fdio = nullptr;
zx_status_t status = fdio_create(file.TakeChannel().release(), &fdio);
if (status == ZX_ERR_PEER_CLOSED) {
// File-not-found errors are expected in some cases, so handle this result
// w/o logging error text.
return false;
} else if (status != ZX_OK) {
ZX_DLOG_IF(WARNING, status != ZX_OK, status) << "fdio_create";
return false;
}
base::ScopedFD fd(fdio_bind_to_fd(fdio, -1, 0));
if (!fd.is_valid()) {
LOG(ERROR) << "fdio_bind_to_fd returned an invalid FD.";
return false;
}
// Map the file into memory.
if (!mmap->Initialize(base::File(fd.release()),
base::MemoryMappedFile::READ_ONLY)) {
return false;
}
return true;
}
// Initiates data transfer from |file_channel| to |client_info|.
// |metadata_channel|, if it is connected to a file, is accessed to get the
// MIME type and charset of the file.
static void CreateAndStart(
network::mojom::URLLoaderRequest url_loader_request,
const network::ResourceRequest& request,
network::mojom::URLLoaderClientPtrInfo client_info,
fidl::InterfaceHandle<fuchsia::io::Node> file_channel,
fidl::InterfaceHandle<fuchsia::io::Node> metadata_channel) {
std::unique_ptr<ContentDirectoryURLLoader> loader =
std::make_unique<ContentDirectoryURLLoader>();
loader->Start(request, std::move(client_info), std::move(file_channel),
std::move(metadata_channel));
// |loader|'s lifetime is bound to the lifetime of the URLLoader Mojo
// client endpoint.
mojo::MakeStrongBinding(std::move(loader), std::move(url_loader_request));
}
void Start(const network::ResourceRequest& request,
network::mojom::URLLoaderClientPtrInfo client_info,
fidl::InterfaceHandle<fuchsia::io::Node> file_channel,
fidl::InterfaceHandle<fuchsia::io::Node> metadata_channel) {
client_.Bind(std::move(client_info));
if (!MapFile(std::move(file_channel), &mmap_)) {
client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
// Construct and deliver the HTTP response header.
network::ResourceResponseHead response;
// Read the charset and MIME type from the optional _metadata file.
base::Optional<std::string> charset;
base::Optional<std::string> mime_type;
base::MemoryMappedFile metadata_mmap;
if (MapFile(std::move(metadata_channel), &metadata_mmap)) {
base::Optional<base::Value> metadata_parsed = base::JSONReader::Read(
base::StringPiece(reinterpret_cast<char*>(metadata_mmap.data()),
metadata_mmap.length()));
if (metadata_parsed && metadata_parsed->is_dict()) {
if (metadata_parsed->FindStringKey("charset"))
charset = *metadata_parsed->FindStringKey("charset");
if (metadata_parsed->FindStringKey("mime"))
mime_type = *metadata_parsed->FindStringKey("mime");
}
}
// If a MIME type wasn't specified, then fall back on inferring the type
// from the file's contents.
if (!mime_type) {
mime_type.emplace();
net::SniffMimeType(
reinterpret_cast<char*>(mmap_.data()), mmap_.length(), request.url,
"", net::ForceSniffFileUrlsForHtml::kDisabled, &*mime_type);
}
response.mime_type = *mime_type;
response.headers = CreateHeaders(response.mime_type, charset);
response.content_length = mmap_.length();
client_->OnReceiveResponse(response);
// Set up the Mojo DataPipe used for streaming the response payload to the
// client.
mojo::ScopedDataPipeProducerHandle producer_handle;
mojo::ScopedDataPipeConsumerHandle consumer_handle;
MojoResult rv = mojo::CreateDataPipe(0, &producer_handle, &consumer_handle);
if (rv != MOJO_RESULT_OK) {
client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
return;
}
client_->OnStartLoadingResponseBody(std::move(consumer_handle));
// Start reading the contents of |mmap_| into the response DataPipe.
body_writer_ =
std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle));
body_writer_->Write(
std::make_unique<mojo::StringDataSource>(
base::StringPiece(reinterpret_cast<char*>(mmap_.data()),
mmap_.length()),
mojo::StringDataSource::AsyncWritingMode::
STRING_STAYS_VALID_UNTIL_COMPLETION),
base::BindOnce(&ContentDirectoryURLLoader::OnWriteComplete,
base::Unretained(this)));
}
// network::mojom::URLLoader implementation:
void FollowRedirect(const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_request_headers,
const base::Optional<GURL>& new_url) override {}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {}
void PauseReadingBodyFromNet() override {}
void ResumeReadingBodyFromNet() override {}
private:
// Called when body_writer_->Write() has completed asynchronously.
void OnWriteComplete(MojoResult result) {
// Signal stream EOF to the client.
body_writer_.reset();
if (result != MOJO_RESULT_OK) {
client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
network::URLLoaderCompletionStatus status(net::OK);
status.encoded_data_length = mmap_.length();
status.encoded_body_length = mmap_.length();
status.decoded_body_length = mmap_.length();
client_->OnComplete(std::move(status));
}
// Used for sending status codes and response payloads to the client.
network::mojom::URLLoaderClientPtr client_;
// A read-only, memory mapped view of the file being loaded.
base::MemoryMappedFile mmap_;
// Manages chunked data transfer over the response DataPipe.
std::unique_ptr<mojo::DataPipeProducer> body_writer_;
DISALLOW_COPY_AND_ASSIGN(ContentDirectoryURLLoader);
};
} // namespace
ContentDirectoryLoaderFactory::ContentDirectoryLoaderFactory()
: task_runner_(base::CreateSequencedTaskRunner(
{base::ThreadPool(), base::MayBlock(),
base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})) {}
ContentDirectoryLoaderFactory::~ContentDirectoryLoaderFactory() {}
net::Error ContentDirectoryLoaderFactory::OpenFileFromDirectory(
const std::string& directory_name,
base::FilePath path,
fidl::InterfaceRequest<fuchsia::io::Node> file_request) {
DCHECK(file_request);
ContentDirectoriesMap* content_directories = GetContentDirectories();
if (content_directories->find(directory_name) == content_directories->end())
return net::ERR_FILE_NOT_FOUND;
const fidl::InterfaceHandle<fuchsia::io::Directory>& directory =
content_directories->at(directory_name);
if (!directory)
return net::ERR_INVALID_HANDLE;
zx_status_t status = fdio_open_at(
directory.channel().get(), path.value().c_str(),
fuchsia::io::OPEN_RIGHT_READABLE, file_request.TakeChannel().release());
if (status != ZX_OK) {
ZX_DLOG(WARNING, status) << "fdio_open_at";
return net::ERR_FILE_NOT_FOUND;
}
return net::OK;
}
void ContentDirectoryLoaderFactory::CreateLoaderAndStart(
network::mojom::URLLoaderRequest loader,
int32_t routing_id,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
network::mojom::URLLoaderClientPtr client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
if (!request.url.SchemeIs(
WebEngineContentClient::kFuchsiaContentDirectoryScheme) ||
!request.url.is_valid()) {
client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INVALID_URL));
return;
}
if (request.method != "GET") {
client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_METHOD_NOT_SUPPORTED));
return;
}
std::string requested_path = request.url.path();
if (requested_path.empty()) {
client->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INVALID_URL));
return;
}
// Not all VFS implementations handle leading slashes in the same way, so
// remove it for maximum compatibility.
// TODO(fxb/38339): Update this comment when issue is resolved.
if (requested_path[0] == '/')
requested_path = requested_path.substr(1, requested_path.size() - 1);
fidl::InterfaceHandle<fuchsia::io::Node> file_handle;
net::Error open_result = OpenFileFromDirectory(request.url.GetOrigin().host(),
base::FilePath(requested_path),
file_handle.NewRequest());
if (open_result != net::OK) {
client->OnComplete(network::URLLoaderCompletionStatus(open_result));
return;
}
DCHECK(file_handle);
// Metadata files are optional. In the event that the file is absent,
// |metadata_channel| will produce a ZX_CHANNEL_PEER_CLOSED status inside
// ContentDirectoryURLLoader::Start().
fidl::InterfaceHandle<fuchsia::io::Node> metadata_handle;
open_result =
OpenFileFromDirectory(request.url.GetOrigin().host(),
base::FilePath(requested_path + "._metadata"),
metadata_handle.NewRequest());
if (open_result != net::OK) {
client->OnComplete(network::URLLoaderCompletionStatus(open_result));
return;
}
// Load the resource on a blocking-capable TaskRunner.
task_runner_->PostTask(FROM_HERE,
base::Bind(&ContentDirectoryURLLoader::CreateAndStart,
base::Passed(std::move(loader)), request,
base::Passed(client.PassInterface()),
base::Passed(std::move(file_handle)),
base::Passed(std::move(metadata_handle))));
}
void ContentDirectoryLoaderFactory::Clone(
mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader) {
receivers_.Add(this, std::move(loader));
}
void ContentDirectoryLoaderFactory::SetContentDirectoriesForTest(
std::vector<fuchsia::web::ContentDirectoryProvider> directories) {
ContentDirectoriesMap* current_process_directories = GetContentDirectories();
current_process_directories->clear();
for (fuchsia::web::ContentDirectoryProvider& directory : directories) {
current_process_directories->emplace(directory.name(),
directory.mutable_directory()->Bind());
}
}