blob: b332c6fc8ebe7012468ac441b084235448366292 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/renderer/extension_localization_throttle.h"
#include "base/containers/span.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "components/crx_file/id_util.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_id.h"
#include "extensions/renderer/extension_frame_helper.h"
#include "extensions/renderer/renderer_extension_registry.h"
#include "extensions/renderer/shared_l10n_map.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "url/gurl.h"
namespace extensions {
namespace {
const char kCancelReason[] = "ExtensionLocalizationThrottle";
// Return the extension id in case it's guid was supplied in the host instead.
ExtensionId GetExtensionIdForGurl(const GURL& gurl) {
if (crx_file::id_util::IdIsValid(gurl.host())) {
return gurl.host();
}
// Find an extension when expecting the host to be a guid.
const Extension* extension =
RendererExtensionRegistry::Get()->GetExtensionOrAppByURL(
gurl, /*include_guid=*/true);
return extension ? extension->id() : gurl.host();
}
class ExtensionLocalizationURLLoader : public network::mojom::URLLoaderClient,
public network::mojom::URLLoader,
public mojo::DataPipeDrainer::Client {
public:
ExtensionLocalizationURLLoader(
const std::optional<blink::LocalFrameToken>& frame_token,
const ExtensionId& extension_id,
mojo::PendingRemote<network::mojom::URLLoaderClient>
destination_url_loader_client)
: frame_token_(frame_token),
extension_id_(extension_id),
destination_url_loader_client_(
std::move(destination_url_loader_client)) {
DCHECK(crx_file::id_util::IdIsValid(extension_id_));
}
~ExtensionLocalizationURLLoader() override = default;
bool Start(
mojo::PendingRemote<network::mojom::URLLoader> source_url_loader_remote,
mojo::PendingReceiver<network::mojom::URLLoaderClient>
source_url_client_receiver,
mojo::ScopedDataPipeConsumerHandle body,
mojo::ScopedDataPipeProducerHandle producer_handle) {
source_url_loader_.Bind(std::move(source_url_loader_remote));
source_url_client_receiver_.Bind(std::move(source_url_client_receiver));
data_drainer_ =
std::make_unique<mojo::DataPipeDrainer>(this, std::move(body));
producer_handle_ = std::move(producer_handle);
return true;
}
// network::mojom::URLLoaderClient implementation (called from the source of
// the response):
void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
// OnReceiveEarlyHints() shouldn't be called because
// ExtensionLocalizationURLLoader is
// created by ExtensionLocalizationThrottle::WillProcessResponse(), which is
// equivalent to OnReceiveResponse().
NOTREACHED();
}
void OnReceiveResponse(
network::mojom::URLResponseHeadPtr response_head,
mojo::ScopedDataPipeConsumerHandle body,
std::optional<mojo_base::BigBuffer> cached_metadata) override {
// OnReceiveResponse() shouldn't be called because
// ExtensionLocalizationURLLoader is
// created by ExtensionLocalizationThrottle::WillProcessResponse(), which is
// equivalent to OnReceiveResponse().
NOTREACHED();
}
void OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
network::mojom::URLResponseHeadPtr response_head) override {
// OnReceiveRedirect() shouldn't be called because
// ExtensionLocalizationURLLoader is
// created by ExtensionLocalizationThrottle::WillProcessResponse(), which is
// equivalent to OnReceiveResponse().
NOTREACHED();
}
void OnUploadProgress(int64_t current_position,
int64_t total_size,
OnUploadProgressCallback ack_callback) override {
// OnUploadProgress() shouldn't be called because
// ExtensionLocalizationURLLoader is
// created by ExtensionLocalizationThrottle::WillProcessResponse(), which is
// equivalent to OnReceiveResponse().
NOTREACHED();
}
void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
destination_url_loader_client_->OnTransferSizeUpdated(transfer_size_diff);
}
void OnComplete(const network::URLLoaderCompletionStatus& status) override {
original_complete_status_ = status;
MaybeSendOnComplete();
}
// network::mojom::URLLoader implementation (called from the destination of
// the response):
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 {
// ExtensionLocalizationURLLoader starts handling the request after
// OnReceivedResponse(). A redirect response is not expected.
NOTREACHED();
}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {
source_url_loader_->SetPriority(priority, intra_priority_value);
}
// mojo::DataPipeDrainer
void OnDataAvailable(base::span<const uint8_t> data) override {
data_.append(base::as_string_view(data));
}
void OnDataComplete() override {
data_drainer_.reset();
if (!data_.empty()) {
ReplaceMessages();
}
auto data_producer =
std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle_));
auto data = std::make_unique<std::string>(std::move(data_));
// To avoid unnecessary string copy, use STRING_STAYS_VALID_UNTIL_COMPLETION
// here, and keep the original data hold in the closure below.
auto source = std::make_unique<mojo::StringDataSource>(
*data, mojo::StringDataSource::AsyncWritingMode::
STRING_STAYS_VALID_UNTIL_COMPLETION);
mojo::DataPipeProducer* data_producer_ptr = data_producer.get();
data_producer_ptr->Write(
std::move(source),
base::BindOnce(
[](std::unique_ptr<mojo::DataPipeProducer> data_producer,
std::unique_ptr<std::string> data,
base::OnceCallback<void(MojoResult)> on_data_written_callback,
MojoResult result) {
std::move(on_data_written_callback).Run(result);
},
std::move(data_producer), std::move(data),
base::BindOnce(&ExtensionLocalizationURLLoader::OnDataWritten,
weak_factory_.GetWeakPtr())));
}
private:
void OnDataWritten(MojoResult result) {
data_write_result_ = result;
MaybeSendOnComplete();
}
void MaybeSendOnComplete() {
if (!original_complete_status_ || !data_write_result_) {
return;
}
if (*data_write_result_ != MOJO_RESULT_OK) {
destination_url_loader_client_->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
} else {
destination_url_loader_client_->OnComplete(*original_complete_status_);
}
}
void ReplaceMessages() {
extensions::SharedL10nMap::IPCTarget* ipc_target = nullptr;
// TODO(dtapuska): content::RenderThread::Get() returns nullptr so the old
// version will never send it to the browser. Figure out why we are even
// doing this on worker threads.
if (content::RenderThread::IsMainThread()) {
content::RenderFrame* render_frame = nullptr;
if (frame_token_) {
if (auto* web_frame =
blink::WebLocalFrame::FromFrameToken(frame_token_.value())) {
render_frame = content::RenderFrame::FromWebFrame(web_frame);
}
}
if (render_frame) {
ipc_target = ExtensionFrameHelper::Get(render_frame)->GetRendererHost();
}
}
extensions::SharedL10nMap::GetInstance().ReplaceMessages(
extension_id_, &data_, ipc_target);
}
const std::optional<blink::LocalFrameToken> frame_token_;
const ExtensionId extension_id_;
std::unique_ptr<mojo::DataPipeDrainer> data_drainer_;
mojo::ScopedDataPipeProducerHandle producer_handle_;
std::string data_;
std::optional<network::URLLoaderCompletionStatus> original_complete_status_;
std::optional<MojoResult> data_write_result_;
mojo::Receiver<network::mojom::URLLoaderClient> source_url_client_receiver_{
this};
mojo::Remote<network::mojom::URLLoader> source_url_loader_;
mojo::Remote<network::mojom::URLLoaderClient> destination_url_loader_client_;
base::WeakPtrFactory<ExtensionLocalizationURLLoader> weak_factory_{this};
};
} // namespace
// static
std::unique_ptr<ExtensionLocalizationThrottle>
ExtensionLocalizationThrottle::MaybeCreate(
base::optional_ref<const blink::LocalFrameToken> local_frame_token,
const GURL& request_url) {
if (!request_url.SchemeIs(extensions::kExtensionScheme)) {
return nullptr;
}
return base::WrapUnique(new ExtensionLocalizationThrottle(local_frame_token));
}
ExtensionLocalizationThrottle::ExtensionLocalizationThrottle(
base::optional_ref<const blink::LocalFrameToken> local_frame_token)
: frame_token_(local_frame_token.CopyAsOptional()) {}
ExtensionLocalizationThrottle::~ExtensionLocalizationThrottle() = default;
void ExtensionLocalizationThrottle::DetachFromCurrentSequence() {}
void ExtensionLocalizationThrottle::WillProcessResponse(
const GURL& response_url,
network::mojom::URLResponseHead* response_head,
bool* defer) {
if (!response_url.SchemeIs(extensions::kExtensionScheme)) {
// The chrome-extension:// URL resource request was redirected by
// webRequest API. In that case, we don't process the response.
return;
}
if (!base::StartsWith(response_head->mime_type, "text/css",
base::CompareCase::INSENSITIVE_ASCII)) {
return;
}
mojo::ScopedDataPipeConsumerHandle body;
mojo::ScopedDataPipeProducerHandle producer_handle;
MojoResult create_pipe_result =
mojo::CreateDataPipe(/*options=*/nullptr, producer_handle, body);
if (create_pipe_result != MOJO_RESULT_OK || force_error_for_test_) {
// Synchronous call of `delegate_->CancelWithError` can cause a UAF error.
// So defer the request here.
*defer = true;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(base::BindOnce(
&ExtensionLocalizationThrottle::DeferredCancelWithError,
weak_factory_.GetWeakPtr(), net::ERR_INSUFFICIENT_RESOURCES)));
return;
}
mojo::PendingRemote<network::mojom::URLLoader> new_remote;
mojo::PendingRemote<network::mojom::URLLoaderClient> url_loader_client;
mojo::PendingReceiver<network::mojom::URLLoaderClient> new_receiver =
url_loader_client.InitWithNewPipeAndPassReceiver();
mojo::PendingRemote<network::mojom::URLLoader> source_loader;
mojo::PendingReceiver<network::mojom::URLLoaderClient> source_client_receiver;
// `response_url.host()` is expected to be the extension id. However, it could
// be a guid e.g. when a web service worker intercepts a guid fetch for css.
ExtensionId extension_id = GetExtensionIdForGurl(response_url);
auto loader = std::make_unique<ExtensionLocalizationURLLoader>(
frame_token_, extension_id, std::move(url_loader_client));
ExtensionLocalizationURLLoader* loader_rawptr = loader.get();
// `loader` will be deleted when `new_remote` is disconnected.
// `new_remote` is bound to ThrottlingURLLoader::url_loader_. So when
// ThrottlingURLLoader is deleted, `loader` will be deleted.
mojo::MakeSelfOwnedReceiver(std::move(loader),
new_remote.InitWithNewPipeAndPassReceiver());
delegate_->InterceptResponse(std::move(new_remote), std::move(new_receiver),
&source_loader, &source_client_receiver, &body);
// ExtensionURLLoader always send a valid DataPipeConsumerHandle. So
// InterceptResponse() must return a valid `body`.
DCHECK(body);
loader_rawptr->Start(std::move(source_loader),
std::move(source_client_receiver), std::move(body),
std::move(producer_handle));
}
void ExtensionLocalizationThrottle::DeferredCancelWithError(int error_code) {
if (delegate_) {
delegate_->CancelWithError(error_code, kCancelReason);
}
}
} // namespace extensions