blob: 7d9d4fa842b93737eadff23c9bd61b9a8ae0b0b5 [file] [log] [blame]
// Copyright 2017 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/api/messaging/one_time_message_handler.h"
#include <algorithm>
#include <map>
#include <memory>
#include <optional>
#include <vector>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ref.h"
#include "base/supports_user_data.h"
#include "content/public/renderer/render_frame.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "extensions/renderer/api/messaging/message_target.h"
#include "extensions/renderer/api/messaging/messaging_util.h"
#include "extensions/renderer/bindings/api_binding_types.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/api_bindings_system.h"
#include "extensions/renderer/bindings/api_event_handler.h"
#include "extensions/renderer/bindings/api_request_handler.h"
#include "extensions/renderer/bindings/get_per_context_data.h"
#include "extensions/renderer/console.h"
#include "extensions/renderer/gc_callback.h"
#include "extensions/renderer/get_script_context.h"
#include "extensions/renderer/ipc_message_sender.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/script_context.h"
#include "gin/arguments.h"
#include "gin/data_object_builder.h"
#include "gin/dictionary.h"
#include "gin/handle.h"
#include "gin/per_context_data.h"
#include "v8/include/v8-container.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-object.h"
#include "v8/include/v8-persistent-handle.h"
#include "v8/include/v8-primitive.h"
namespace extensions {
namespace {
// An opener port in the context; i.e., the caller of runtime.sendMessage.
struct OneTimeOpener {
int request_id = -1;
binding::AsyncResponseType async_type = binding::AsyncResponseType::kNone;
mojom::ChannelType channel_type;
};
// A receiver port in the context; i.e., a listener to runtime.onMessage.
struct OneTimeReceiver {
std::string event_name;
v8::Global<v8::Object> sender;
v8::Global<v8::Function> message_response_function;
};
struct OneTimeMessageContextData : public base::SupportsUserData::Data {
static constexpr char kPerContextDataKey[] =
"extension_one_time_message_context_data";
std::map<PortId, OneTimeOpener> openers;
// If the receiver is still present for `PortId`, then a response can still be
// sent from the listener to the message sender. Otherwise no response (or
// error) should be sent back to the message sender from the listener.
std::map<PortId, OneTimeReceiver> receivers;
using OneTimePortCallbacks =
std::map<OneTimeMessageHandler::CallbackID,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>>;
// Owns the pending callbacks used for message replies. A listener's v8
// context may invoke a response callback asynchronously. This map keeps the
// callback alive until it is invoked or the connection is closed. Note: this
// struct is accessed by `OneTimeMessageHandler` but this collection is
// conceptually owned by
// `OneTimeMessageHandler::OneTimeMessageCallbackManager`. It is placed in
// this struct for simplicity since the classes are so interrelated.
std::map<PortId, OneTimePortCallbacks> pending_receiver_callbacks;
};
constexpr char OneTimeMessageContextData::kPerContextDataKey[];
bool IsMessagePolyfillSupportEnabled() {
return base::FeatureList::IsEnabled(
extensions_features::kRuntimeOnMessageWebExtensionPolyfillSupport);
}
// Returns an array from the `result` object's `property_name` if it exists,
// otherwise returns an empty `v8::Local<v8::Array>`.
v8::Local<v8::Array> GetListenerResultArray(v8::Isolate* isolate,
v8::Local<v8::Context> context,
v8::Local<v8::Value> result,
const char* property_name) {
// `result` can be undefined if the context was destroyed before the
// listeners were run (or while they were running).
if (result->IsUndefined()) {
return v8::Local<v8::Array>();
}
// We expect results as a value with an array of results as a `property_name`
// property, however, since this comes from untrusted JS let's confirm this
// first.
if (!result->IsObject()) {
return v8::Local<v8::Array>();
}
v8::Local<v8::Object> result_object = result.As<v8::Object>();
v8::Local<v8::Value> array_value;
if (!result_object->Get(context, gin::StringToSymbol(isolate, property_name))
.ToLocal(&array_value) ||
!array_value->IsArray()) {
return v8::Local<v8::Array>();
}
return array_value.As<v8::Array>();
}
void DelayedOneTimeMessageCallbackHelper(
const v8::FunctionCallbackInfo<v8::Value>& info) {
CHECK(info.Data()->IsString());
gin::Arguments arguments(info);
v8::Isolate* isolate = arguments.isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data)
return;
// Retrieve the CallbackID from v8 that we set when we created the callback.
v8::Local<v8::String> callback_id_v8_string = info.Data().As<v8::String>();
std::string callback_id_string;
if (!gin::Converter<std::string>::FromV8(isolate, callback_id_v8_string,
&callback_id_string)) {
return;
}
std::optional<OneTimeMessageHandler::CallbackID> callback_id =
OneTimeMessageHandler::CallbackID::DeserializeFromString(
callback_id_string);
if (!callback_id) {
// Something must've changed this value unexpectedly. In any case we don't
// know which callback to run so don't run any callbacks. If this was
// intended for a pending callback that is still in
// `data->pending_receiver_callbacks`, it will be garbage collected later in
// `OnDelayedOneTimeMessageCallbackCollected()`.
return;
}
// Search each `PortId` in `data->pending_receiver_callbacks` to see if any of
// them have `callback_id`.
OneTimeMessageContextData::OneTimePortCallbacks* port_callbacks = nullptr;
OneTimeMessageContextData::OneTimePortCallbacks::iterator port_callback_iter;
for (auto& port_entry : data->pending_receiver_callbacks) {
auto callback_entry = port_entry.second.find(*callback_id);
if (callback_entry == port_entry.second.end()) {
// `callback_id` is not associated with this `PortId`.
continue;
}
// Found the callback for this `callback_id`. There shouldn't be any
// duplicates so stop searching.
port_callbacks = &port_entry.second;
port_callback_iter = callback_entry;
break;
}
// Couldn't find `callback_id` amongst the `PortId`s for this extension.
if (!port_callbacks) {
// One way this can happen is if an extension attempts to respond to a
// message multiple times despite us only allowing the first response to
// be sent back to the sender. If that happens, just return early to
// enforce this.
return;
}
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback =
std::move(port_callback_iter->second);
port_callbacks->erase(port_callback_iter);
std::move(*callback).Run(&arguments);
}
} // namespace
// A helper class to manage the creation and tracking of callbacks for
// one-time messages, such as the message response callback.
//
// This class creates `v8::Function`s that are associated to a C++ callback
// (`OneTimeMessageCallback`) and handles the cleanup of associated resources
// when the `v8::Function` is garbage collected. This allows message listeners
// to reply asynchronously without leaking resources.
//
// An instance of this class is held by the `OneTimeMessageHandler` and its
// lifetime is tied to the handler.
class OneTimeMessageHandler::OneTimeMessageCallbackManager {
public:
explicit OneTimeMessageCallbackManager(
OneTimeMessageHandler& owning_message_handler);
~OneTimeMessageCallbackManager();
OneTimeMessageCallbackManager(const OneTimeMessageCallbackManager&) = delete;
OneTimeMessageCallbackManager& operator=(
const OneTimeMessageCallbackManager&) = delete;
// Returns a v8 function that will call `callback` when the the function is
// called in v8. `callback` will be cleaned up when the returned function is
// garbage collected by v8.
v8::Local<v8::Function> CreateRespondingFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback);
// Returns a v8 function that will call `callback` after the sender's
// message is dispatched to all message listeners. `callback` will *not* be
// cleaned up when the returned function is garbage collected by v8 since it
// is expected to always be called synchronously immediately after event
// dispatch.
v8::Local<v8::Function> CreateEventDispatchFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback);
// Returns a v8 function that will call `callback` whenever a listener in the
// receiver throws a synchronous error. `callback` will *not* be cleaned up
// when the returned function is garbage collected by v8 since it is expected
// to cleaned up by either a) being called, or b) being deleted after all
// listeners have been dispatched to.
// `callback_id` is the unique ID of `callback` for later retrieval when the
// returned function calls `callback.`
v8::Local<v8::Function> CreateListenerThrowsErrorFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback,
const CallbackID& callback_id);
// Clears any pending `OneTimeMessageHandler::OneTimeMessageCallback`s that
// could be called for `port_id`.
void ClearCallbackDataForPortId(ScriptContext* script_context,
const PortId& port_id);
// Deletes `port_id`'s `OneTimeMessageHandler::OneTimeMessageCallback` that is
// identified by `callback_id` .
void DeleteCallbackDataForCallbackId(
ScriptContext* script_context,
const PortId& port_id,
const OneTimeMessageHandler::CallbackID& callback_id);
// Gets the number of pending callbacks for the `port_id` on the associated
// per context data for testing purposes.
int GetPendingCallbackCountForTest(ScriptContext* script_context, // IN-TEST
const PortId& port_id);
private:
// Helper method for creating delayed callbacks that can be called as a
// result of message listener behavior. `cleanup_if_function_unused` true
// means that, if the context is still valid when the `v8::Function` that is
// created to call `callback` is garbage collected, we'll cleanup
// `callback`.
// If polyfill support is enabled we'll notify `OneTimeMessageHandler` if
// there are no more `OneTimeMessageHandler::OneTimeMessageCallback`s to
// collect, otherwise we'll notify of the one and only callback (message
// response) collection.
// `optional_callback_id` allows the caller to specify the
// `OneTimeMessageHandler::CallbackID` used to identify the callback,
// otherwise one will be generated for them.
v8::Local<v8::Function> CreateDelayedOneTimeMessageCallback(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback,
bool cleanup_if_function_unused);
v8::Local<v8::Function> CreateDelayedOneTimeMessageCallback(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback,
bool cleanup_if_function_unused,
std::optional<CallbackID> optional_callback_id);
// Triggered when a `v8::Function` that had `cleanup_if_function_unused` set
// to true when it was created is no longer accessible in the context and v8
// has garbage collected it.
// Used to clean up data that was stored for the `v8::Function` (the
// `OneTimeMessageHandler::OneTimeMessageCallback` it is associated with)
// and for closing the associated message port. `callback_id` is the ID of
// the associated `OneTimeMessageHandler::OneTimeMessageCallback`, needed
// for finding and erasing it from the OneTimeMessageContextData.
void OnDelayedOneTimeMessageCallbackCollected(
ScriptContext* script_context,
const PortId& port_id,
OneTimeMessageHandler::CallbackID callback_id);
// The owning OneTimeMessageHandler. Outlives this object.
const raw_ref<OneTimeMessageHandler> message_handler_;
base::WeakPtrFactory<OneTimeMessageHandler::OneTimeMessageCallbackManager>
weak_factory_{this};
};
OneTimeMessageHandler::OneTimeMessageCallbackManager::
OneTimeMessageCallbackManager(OneTimeMessageHandler& owning_message_handler)
: message_handler_(owning_message_handler) {}
OneTimeMessageHandler::OneTimeMessageCallbackManager::
~OneTimeMessageCallbackManager() = default;
v8::Local<v8::Function>
OneTimeMessageHandler::OneTimeMessageCallbackManager::CreateRespondingFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback) {
return CreateDelayedOneTimeMessageCallback(
script_context, port_id, std::move(callback),
/*cleanup_if_function_unused=*/true);
}
v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager::
CreateEventDispatchFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>
callback) {
return CreateDelayedOneTimeMessageCallback(
script_context, port_id, std::move(callback),
/*cleanup_if_function_unused=*/false);
}
v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager::
CreateListenerThrowsErrorFunction(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback,
const CallbackID& callback_id) {
return CreateDelayedOneTimeMessageCallback(
script_context, port_id, std::move(callback),
/*cleanup_if_function_unused=*/false, callback_id);
}
void OneTimeMessageHandler::OneTimeMessageCallbackManager::
ClearCallbackDataForPortId(ScriptContext* script_context,
const PortId& port_id) {
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
if (!data) {
return;
}
data->pending_receiver_callbacks.erase(port_id);
}
void OneTimeMessageHandler::OneTimeMessageCallbackManager::
DeleteCallbackDataForCallbackId(
ScriptContext* script_context,
const PortId& port_id,
const OneTimeMessageHandler::CallbackID& callback_id) {
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
if (!data) {
return;
}
if (auto port_iter = data->pending_receiver_callbacks.find(port_id);
port_iter != data->pending_receiver_callbacks.end()) {
port_iter->second.erase(callback_id);
}
}
int OneTimeMessageHandler::OneTimeMessageCallbackManager::
GetPendingCallbackCountForTest(ScriptContext* script_context,
const PortId& port_id) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
if (!data) {
return 0;
}
if (auto port_iter = data->pending_receiver_callbacks.find(port_id);
port_iter != data->pending_receiver_callbacks.end()) {
return port_iter->second.size();
}
return 0;
}
OneTimeMessageHandler::OneTimeMessageHandler(
NativeExtensionBindingsSystem* bindings_system)
: bindings_system_(bindings_system),
callback_manager_(
std::make_unique<
OneTimeMessageHandler::OneTimeMessageCallbackManager>(*this)) {}
OneTimeMessageHandler::~OneTimeMessageHandler() = default;
bool OneTimeMessageHandler::HasPort(ScriptContext* script_context,
const PortId& port_id) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
if (!data)
return false;
return port_id.is_opener ? base::Contains(data->openers, port_id)
: base::Contains(data->receivers, port_id);
}
v8::Local<v8::Promise> OneTimeMessageHandler::SendMessage(
ScriptContext* script_context,
const PortId& new_port_id,
const MessageTarget& target,
mojom::ChannelType channel_type,
const Message& message,
binding::AsyncResponseType async_type,
v8::Local<v8::Function> response_callback,
mojom::MessagePortHost* message_port_host,
mojo::PendingAssociatedRemote<mojom::MessagePort> message_port,
mojo::PendingAssociatedReceiver<mojom::MessagePortHost>
message_port_host_receiver) {
v8::Isolate* isolate = script_context->isolate();
v8::EscapableHandleScope handle_scope(isolate);
DCHECK(new_port_id.is_opener);
DCHECK_EQ(script_context->context_id(), new_port_id.context_id);
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kCreateIfMissing);
DCHECK(data);
v8::Local<v8::Promise> promise;
bool wants_response = async_type != binding::AsyncResponseType::kNone;
if (wants_response) {
// If this is a promise based request no callback should have been passed
// in.
if (async_type == binding::AsyncResponseType::kPromise)
DCHECK(response_callback.IsEmpty());
APIRequestHandler::RequestDetails details =
bindings_system_->api_system()->request_handler()->AddPendingRequest(
script_context->v8_context(), async_type, response_callback,
binding::ResultModifierFunction());
OneTimeOpener& port = data->openers[new_port_id];
port.request_id = details.request_id;
port.async_type = async_type;
port.channel_type = channel_type;
promise = details.promise;
DCHECK_EQ(async_type == binding::AsyncResponseType::kPromise,
!promise.IsEmpty());
}
IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
std::string channel_name;
switch (channel_type) {
case mojom::ChannelType::kSendRequest:
channel_name = messaging_util::kSendRequestChannel;
break;
case mojom::ChannelType::kSendMessage:
channel_name = messaging_util::kSendMessageChannel;
break;
case mojom::ChannelType::kNative:
// Native messaging doesn't use channel names.
break;
case mojom::ChannelType::kConnect:
// connect() calls aren't handled by the OneTimeMessageHandler.
NOTREACHED();
}
ipc_sender->SendOpenMessageChannel(
script_context, new_port_id, target, channel_type, channel_name,
std::move(message_port), std::move(message_port_host_receiver));
message_port_host->PostMessage(message);
// If the sender doesn't provide a response callback, we can immediately
// close the channel. Note: we only do this for extension messages, not
// native apps.
// TODO(devlin): This is because of some subtle ordering in the browser side,
// where closing the channel after sending the message causes things to be
// destroyed in the wrong order. That would be nice to fix.
if (!wants_response && target.type != MessageTarget::NATIVE_APP) {
message_port_host->ClosePort(/*close_channel=*/true,
/*error_message=*/std::nullopt);
}
return handle_scope.Escape(promise);
}
void OneTimeMessageHandler::AddReceiver(ScriptContext* script_context,
const PortId& target_port_id,
v8::Local<v8::Object> sender,
const std::string& event_name) {
DCHECK(!target_port_id.is_opener);
DCHECK_NE(script_context->context_id(), target_port_id.context_id);
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = script_context->v8_context();
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context, kCreateIfMissing);
DCHECK(data);
DCHECK(!base::Contains(data->receivers, target_port_id));
OneTimeReceiver& receiver = data->receivers[target_port_id];
receiver.sender.Reset(isolate, sender);
receiver.event_name = event_name;
}
void OneTimeMessageHandler::AddReceiverForTesting(
ScriptContext* script_context,
const PortId& target_port_id,
v8::Local<v8::Object> sender,
const std::string& event_name,
mojo::PendingAssociatedRemote<mojom::MessagePort>& message_port_remote,
mojo::PendingAssociatedReceiver<mojom::MessagePortHost>&
message_port_host_receiver) {
AddReceiver(script_context, target_port_id, sender, event_name);
messaging_service()->BindPortForTesting( // IN-TEST
script_context, target_port_id, message_port_remote,
message_port_host_receiver);
}
bool OneTimeMessageHandler::DeliverMessage(ScriptContext* script_context,
const Message& message,
const PortId& target_port_id) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
return target_port_id.is_opener
? DeliverReplyToOpener(script_context, message, target_port_id)
: DeliverMessageToReceiver(script_context, message,
target_port_id);
}
bool OneTimeMessageHandler::Disconnect(ScriptContext* script_context,
const PortId& port_id,
const std::string& error_message) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
return port_id.is_opener
? DisconnectOpener(script_context, port_id, error_message)
: DisconnectReceiver(script_context, port_id);
}
int OneTimeMessageHandler::GetPendingCallbackCountForTest(
ScriptContext* script_context,
const PortId& port_id) {
return callback_manager_->GetPendingCallbackCountForTest( // IN-TEST
script_context, port_id);
}
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>
OneTimeMessageHandler::CreateMessageResponseCallback(const PortId& port_id) {
return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>(
base::BindOnce(&OneTimeMessageHandler::OnOneTimeMessageResponse,
weak_factory_.GetWeakPtr(), port_id));
}
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>
OneTimeMessageHandler::CreatePromiseRejectedCallback(const PortId& port_id) {
return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>(
base::BindOnce(&OneTimeMessageHandler::OnPromiseRejectedResponse,
weak_factory_.GetWeakPtr(), port_id));
}
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>
OneTimeMessageHandler::CreateEventDispatchCallback(
const PortId& port_id,
std::optional<CallbackID> listener_error_callback_id) {
return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>(
base::BindOnce(&OneTimeMessageHandler::OnEventFired,
weak_factory_.GetWeakPtr(), port_id,
listener_error_callback_id));
}
std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>
OneTimeMessageHandler::CreateListenerErrorCallback(const PortId& port_id) {
return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>(
base::BindOnce(&OneTimeMessageHandler::OnListenerThrowsError,
weak_factory_.GetWeakPtr(), port_id));
}
void OneTimeMessageHandler::OnAllCallbacksCollected(
ScriptContext* script_context,
v8::Local<v8::Context> context,
const PortId& port_id) {
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
if (!data) {
return;
}
auto iter = data->receivers.find(port_id);
// The channel may already be closed (if the receiver replied before the reply
// callback was collected).
if (iter == data->receivers.end()) {
return;
}
// Since no more callbacks can be called the receiver doesn't need to be
// tracked anymore.
data->receivers.erase(port_id);
// A different receiver may reply so don't close the channel.
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/false,
/*error=*/std::nullopt);
}
bool OneTimeMessageHandler::DeliverMessageToReceiver(
ScriptContext* script_context,
const Message& message,
const PortId& target_port_id) {
DCHECK(!target_port_id.is_opener);
v8::Isolate* isolate = script_context->isolate();
v8::Local<v8::Context> context = script_context->v8_context();
bool handled = false;
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data)
return handled;
auto iter = data->receivers.find(target_port_id);
if (iter == data->receivers.end())
return handled;
handled = true;
OneTimeReceiver& port = iter->second;
// This port is a receiver, so we invoke the onMessage event and provide a
// callback through which the port can respond. The port stays open until we
// receive a response.
auto message_response_callback =
CreateMessageResponseCallback(target_port_id);
// The v8 `reply` (a.k.a `sendResponse()`) function provided to
// `runtime.onMessage` listeners.
v8::Local<v8::Function> message_response_function =
callback_manager_->CreateRespondingFunction(
*script_context, target_port_id,
std::move(message_response_callback));
if (IsMessagePolyfillSupportEnabled()) {
port.message_response_function =
v8::Global<v8::Function>(isolate, message_response_function);
}
v8::HandleScope handle_scope(isolate);
// The current port is a receiver. The parsing should be fail-safe if this is
// a receiver for a native messaging host (i.e. the event name is
// kOnConnectNativeEvent). This is because a native messaging host can send
// malformed messages.
std::string error;
v8::Local<v8::Value> v8_message = messaging_util::MessageToV8(
context, message,
port.event_name == messaging_util::kOnConnectNativeEvent, &error);
if (error.empty()) {
v8::Local<v8::Object> v8_sender = port.sender.Get(isolate);
v8::LocalVector<v8::Value> args(
isolate, {v8_message, v8_sender, message_response_function});
v8::Local<v8::Function> message_dispatched_function;
v8::Local<v8::Function> listener_throws_error_function;
// For runtime.onMessage, we require that the listener indicate if they
// intend to respond asynchronously. `message_dispatched_callback` will
// check the results of the listeners to determine if a listener indicated
// it intended to respond asynchronously.
if (port.event_name == messaging_util::kOnMessageEvent) {
CallbackID listener_throws_error_callback_id;
if (IsMessagePolyfillSupportEnabled()) {
auto listener_throws_error_callback =
CreateListenerErrorCallback(target_port_id);
listener_throws_error_callback_id = CallbackID::Create();
listener_throws_error_function =
callback_manager_->CreateListenerThrowsErrorFunction(
*script_context, target_port_id,
std::move(listener_throws_error_callback),
listener_throws_error_callback_id);
}
auto message_dispatched_callback = CreateEventDispatchCallback(
target_port_id, listener_throws_error_callback_id);
message_dispatched_function =
callback_manager_->CreateEventDispatchFunction(
*script_context, target_port_id,
std::move(message_dispatched_callback));
}
bindings_system_->api_system()->event_handler()->FireEventInContext(
port.event_name, context, &args, /*filter=*/nullptr,
message_dispatched_function, listener_throws_error_function);
} else {
console::AddMessage(script_context,
blink::mojom::ConsoleMessageLevel::kError, error);
}
// Note: The context could be invalidated at this point!
return handled;
}
bool OneTimeMessageHandler::DeliverReplyToOpener(ScriptContext* script_context,
const Message& message,
const PortId& target_port_id) {
DCHECK(target_port_id.is_opener);
v8::Local<v8::Context> v8_context = script_context->v8_context();
bool handled = false;
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(v8_context,
kDontCreateIfMissing);
if (!data)
return handled;
auto iter = data->openers.find(target_port_id);
if (iter == data->openers.end())
return handled;
handled = true;
// Note: make a copy of port, since we're about to free it.
const OneTimeOpener port = iter->second;
DCHECK_NE(-1, port.request_id);
// We erase the opener now, since delivering the reply can cause JS to run,
// which could either invalidate the context or modify the |openers|
// collection (e.g., by sending another message).
data->openers.erase(iter);
// This port was the opener, so the message is the response from the
// receiver. Invoke the callback and close the message port.
v8::Isolate* isolate = script_context->isolate();
// Parsing should be fail-safe for kNative channel type as native messaging
// hosts can send malformed messages.
std::string error;
v8::Local<v8::Value> v8_message = messaging_util::MessageToV8(
v8_context, message, port.channel_type == mojom::ChannelType::kNative,
&error);
if (v8_message.IsEmpty()) {
// If the parsing fails, send back a v8::Undefined() message.
v8_message = v8::Undefined(isolate);
}
v8::LocalVector<v8::Value> args(isolate, {v8_message});
bindings_system_->api_system()->request_handler()->CompleteRequest(
port.request_id, args, error);
bindings_system_->messaging_service()->CloseMessagePort(
script_context, target_port_id, /*close_channel=*/true);
// Note: The context could be invalidated at this point!
return handled;
}
bool OneTimeMessageHandler::DisconnectReceiver(ScriptContext* script_context,
const PortId& port_id) {
v8::Local<v8::Context> context = script_context->v8_context();
bool handled = false;
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data)
return handled;
auto iter = data->receivers.find(port_id);
if (iter == data->receivers.end())
return handled;
handled = true;
// With the channel closed, clean up the receiver port and its pending
// callbacks. This prevents further responses and avoids callback data leaks
// from indicated-but-never-sent asynchronous replies from the listener(s).
data->receivers.erase(iter);
callback_manager_->ClearCallbackDataForPortId(script_context, port_id);
// The `ExtensionMessagePort` for this receiver's destructor handles message
// port (IPC) cleanup so we don't need to do that here.
return handled;
}
bool OneTimeMessageHandler::DisconnectOpener(ScriptContext* script_context,
const PortId& port_id,
const std::string& error_message) {
bool handled = false;
v8::Local<v8::Context> v8_context = script_context->v8_context();
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(v8_context,
kDontCreateIfMissing);
if (!data)
return handled;
auto iter = data->openers.find(port_id);
if (iter == data->openers.end())
return handled;
handled = true;
// Note: make a copy of port, since we're about to free it.
const OneTimeOpener opener = iter->second;
DCHECK_NE(-1, opener.request_id);
// We erase the opener now, since delivering the reply can cause JS to run,
// which could either invalidate the context or modify the |openers|
// collection (e.g., by sending another message).
data->openers.erase(iter);
std::string error;
// Set the error for the message port. If the browser supplies an error, we
// always use that. Otherwise, the behavior is different for promise-based vs
// callback-based channels.
// For a promise-based channel, not receiving a response is fine (assuming the
// listener didn't indicate it would send one) - the extension may simply be
// waiting for confirmation that the message sent.
// In the callback-based scenario, we use the presence of the callback as an
// indication that the extension expected a specific response. This is an
// unfortunate behavior difference that we keep for backwards-compatibility in
// callback-based API calls.
if (!error_message.empty()) {
// If the browser supplied us with an error message, use that.
error = error_message;
} else if (opener.async_type == binding::AsyncResponseType::kCallback) {
error = "The message port closed before a response was received.";
}
bindings_system_->api_system()->request_handler()->CompleteRequest(
opener.request_id, v8::LocalVector<v8::Value>(v8::Isolate::GetCurrent()),
error);
// Note: The context could be invalidated at this point!
return handled;
}
void OneTimeMessageHandler::CloseReceiverMessagePortOrChannel(
ScriptContext* script_context,
const PortId& port_id,
bool close_channel,
std::optional<std::string> error) {
// With the message port closing callbacks aren't allowed to be called after
// this point so proactively clean them up.
callback_manager_->ClearCallbackDataForPortId(script_context, port_id);
// If there was an error send it back to the message sender.
if (close_channel && error) {
messaging_service()->CloseMessagePort(script_context, port_id,
close_channel, *error);
return;
}
// Otherwise if no error then just close the port and/or channel.
messaging_service()->CloseMessagePort(script_context, port_id, close_channel);
}
void OneTimeMessageHandler::OnOneTimeMessageResponse(
const PortId& port_id,
gin::Arguments* arguments) {
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// The listener may try replying after the context or the channel has been
// closed. Fail gracefully.
// TODO(devlin): At least in the case of the channel being closed (e.g.
// because the listener did not indicate it would reply asynchronously), it
// might be good to surface an error.
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data)
return;
auto iter = data->receivers.find(port_id);
// The channel may already be closed (if a listener replied (promise rejected)
// or listener threw error).
if (iter == data->receivers.end())
return;
// The response will be sent after this point so we no longer need to track
// the receiver.
data->receivers.erase(port_id);
v8::Local<v8::Value> value;
// We allow omitting the message argument (e.g., sendMessage()). Default the
// value to undefined.
if (arguments->Length() > 0)
CHECK(arguments->GetNext(&value));
else
value = v8::Undefined(isolate);
ScriptContext* script_context = GetScriptContextFromV8Context(context);
std::string message_creation_error;
std::unique_ptr<Message> message = messaging_util::MessageFromV8(
context, value, port_id.serialization_format, &message_creation_error);
if (!message) {
// Throw an error in the listener context.
arguments->ThrowTypeError(message_creation_error);
if (IsMessagePolyfillSupportEnabled()) {
// This is a "fatal" error for the channel so close it entirely.
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/true,
message_creation_error);
}
return;
}
// If the MessagePortHost is still alive return the response. But the listener
// might be replying after the channel has been closed.
if (auto* message_port_host = messaging_service()->GetMessagePortHostIfExists(
script_context, port_id)) {
message_port_host->PostMessage(*message);
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/true,
/*error=*/std::nullopt);
}
// With the message port closed no more callbacks should be called.
callback_manager_->ClearCallbackDataForPortId(script_context, port_id);
}
v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager::
CreateDelayedOneTimeMessageCallback(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageCallback> callback,
bool cleanup_if_function_unused) {
return CreateDelayedOneTimeMessageCallback(
script_context, port_id, std::move(callback), cleanup_if_function_unused,
/*optional_callback_id=*/std::nullopt);
}
v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager::
CreateDelayedOneTimeMessageCallback(
ScriptContext& script_context,
const PortId& port_id,
std::unique_ptr<OneTimeMessageCallback> callback,
bool cleanup_if_function_unused,
std::optional<CallbackID> optional_callback_id) {
CHECK(callback);
v8::Isolate* isolate = script_context.isolate();
v8::Local<v8::Context> context = script_context.v8_context();
// We shouldn't need to check and get `data` like this if a listener has
// already responded, but it's much simpler to re-get it here than pass
// OneTimeMessageContextData into this method.
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(
context, CreatePerContextData::kDontCreateIfMissing);
// We will store `callback` in the per context data for later retrieval so it
// must exist for us to proceed.
CHECK(data);
CallbackID callback_id;
if (optional_callback_id) {
callback_id = *optional_callback_id;
} else {
callback_id = OneTimeMessageHandler::CallbackID::Create();
}
// We convert to a v8::String here because we want to validate the string is
// still a valid `CallbackID` when we retrieve it from v8 when `function` is
// called.
v8::Local<v8::String> callback_id_v8_string =
gin::StringToV8(isolate, callback_id.ToString());
v8::Local<v8::Function> function;
if (!v8::Function::New(context, &DelayedOneTimeMessageCallbackHelper,
callback_id_v8_string)
.ToLocal(&function)) {
NOTREACHED();
}
auto& port_callbacks = data->pending_receiver_callbacks[port_id];
const auto& [callback_id_iter, callback_id_inserted] =
port_callbacks.try_emplace(callback_id, std::move(callback));
// It could lead to unexpected behavior to add the same callback multiple
// times for the same one time message port.
CHECK(callback_id_inserted);
if (cleanup_if_function_unused) {
new GCCallback(
&script_context, function,
/*callback=*/
base::BindOnce(&OneTimeMessageHandler::OneTimeMessageCallbackManager::
OnDelayedOneTimeMessageCallbackCollected,
weak_factory_.GetWeakPtr(), &script_context, port_id,
callback_id),
/*fallback=*/base::OnceClosure());
}
return function;
}
void OneTimeMessageHandler::OneTimeMessageCallbackManager::
OnDelayedOneTimeMessageCallbackCollected(ScriptContext* script_context,
const PortId& port_id,
CallbackID callback_id) {
// Note: we know |script_context| is still valid because the GC callback won't
// be called after context invalidation.
v8::HandleScope handle_scope(script_context->isolate());
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
kDontCreateIfMissing);
// ScriptContext invalidation and PerContextData cleanup happen "around" the
// same time, but there aren't strict guarantees about ordering. It's possible
// the data was collected.
if (!data)
return;
// Since there is no way to call the callback anymore, we can remove it from
// the pending callbacks and delete the port entry if this was the last
// callback. Note: this should occur before returning early due to the
// receiver being deleted because multiple pending callbacks can be created
// for each message or `DisconnectReceiver()` could be called before we get
// here.
if (auto port_id_iter = data->pending_receiver_callbacks.find(port_id);
port_id_iter != data->pending_receiver_callbacks.end()) {
auto& callbacks = port_id_iter->second;
callbacks.erase(callback_id);
if (!callbacks.empty()) {
// If we've deleted the callback, but there's still a remaining callback
// then this should only happen iff polyfill support is enabled.
DCHECK(IsMessagePolyfillSupportEnabled());
// When polyfill support is enabled we'll create two callbacks (message
// response and promise reject) that can be collected at different times.
// Only the last callback of these two collected should continue on to
// close the port. Otherwise it could cause the other callback to not
// fully run if called because it'll think the port was already closed.
return;
}
// There are no more callbacks remaining, so delete the unused `PortId` key.
data->pending_receiver_callbacks.erase(port_id_iter);
}
// Notify `message_handler_` so it can update the port state.
message_handler_->OnAllCallbacksCollected(
script_context, script_context->v8_context(), port_id);
// More callbacks could be collected later so we'll leave the callback data
// alone after closing the port.
}
std::optional<std::string> OneTimeMessageHandler::GetErrorMessageFromValue(
v8::Isolate* isolate,
v8::Local<v8::Value> possible_error_value) {
if (!possible_error_value->IsNativeError()) {
return std::nullopt;
}
v8::Local<v8::Message> error_message =
v8::Exception::CreateMessage(isolate, possible_error_value);
std::string error_message_from_v8;
bool error_message_string_convert_success =
gin::Converter<std::string>::FromV8(isolate,
error_message->Get().As<v8::Value>(),
&error_message_from_v8);
if (!error_message_string_convert_success || error_message_from_v8.empty()) {
return std::nullopt;
}
return error_message_from_v8;
}
void OneTimeMessageHandler::OnPromiseRejectedResponse(
const PortId& port_id,
gin::Arguments* arguments) {
CHECK(IsMessagePolyfillSupportEnabled());
CHECK(arguments);
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// The promise may reject after the context or the channel has been closed.
// Fail gracefully.
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data) {
return;
}
auto iter = data->receivers.find(port_id);
// The channel may already be closed (if a listener already replied, or
// listener threw error).
if (iter == data->receivers.end()) {
return;
}
// The promise reject will be sent as an error response after this point so we
// no longer need to track the receiver.
data->receivers.erase(port_id);
v8::Local<v8::Value> promise_reject_value;
// This is safe to CHECK() because when a promise rejects it always provides a
// value. Even if `reject()` (with no argument) is called we see `undefined`
// for `promise_reject_value`.
CHECK(arguments->Length() > 0);
CHECK(arguments->GetNext(&promise_reject_value));
// If promise rejection reason is a JS Error type then close the message port
// with the Error's .message property. Otherwise return a generic error
// message.
// TODO(crbug.com/439644930): Support sending the listener's stack trace along
// with the rejection error. mozilla/webextension-polyfill doesn't support it
// currently, but plans to (see
// https://github.com/mozilla/webextension-polyfill/issues/210).
std::optional<std::string> error_message_from_value =
GetErrorMessageFromValue(isolate, promise_reject_value);
std::string error_message =
error_message_from_value
? *error_message_from_value
: "A runtime.onMessage listener's promise rejected without an Error";
// TODO(crbug.com/439644930): Support sending the listener's stack trace along
// with the rejection error. mozilla/webextension-polyfill doesn't support it
// currently, but plans to (see
// https://github.com/mozilla/webextension-polyfill/issues/210).
ScriptContext* script_context = GetScriptContextFromV8Context(context);
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/true, error_message);
}
void OneTimeMessageHandler::OnListenerThrowsError(const PortId& port_id,
gin::Arguments* arguments) {
CHECK(IsMessagePolyfillSupportEnabled());
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
// Dispatching can invalidate the context so if it is then we won't be able to
// inform the message sender.
if (!data) {
return;
}
auto iter = data->receivers.find(port_id);
// The channel may already be closed (if a listener already replied).
if (iter == data->receivers.end()) {
return;
}
// The listener thrown error will be sent as an error response after this
// point so we no longer need to track the receiver.
data->receivers.erase(port_id);
v8::Local<v8::Value> listener_thrown_value;
CHECK(arguments->Length() > 0);
CHECK(arguments->GetNext(&listener_thrown_value));
std::optional<std::string> error_message_from_value =
GetErrorMessageFromValue(isolate, listener_thrown_value);
std::string error_message =
error_message_from_value
? *error_message_from_value
: "Error message from listener couldn't be parsed or was empty.";
// TODO(crbug.com/439644930): Support sending the listener's stack trace along
// with the rejection error. mozilla/webextension-polyfill doesn't support it
// currently, but plans to (see
// https://github.com/mozilla/webextension-polyfill/issues/210).
ScriptContext* script_context = GetScriptContextFromV8Context(context);
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/true, error_message);
}
bool OneTimeMessageHandler::CheckAndHandleAsyncListenerReply(
v8::Isolate* isolate,
v8::Local<v8::Context> context,
ScriptContext& script_context,
v8::Local<v8::Value> result,
const PortId& port_id,
// TODO(crbug.com/40753031): Move the creation of
// `promise_resolved_function` to just before promise handler attachment. It
// doesn't need to be created before that point.
v8::Local<v8::Function> promise_resolved_function) {
v8::Local<v8::Array> results_array =
GetListenerResultArray(isolate, context, result, "results");
if (results_array.IsEmpty()) {
return false;
}
bool will_reply_async = false;
for (uint32_t i = 0; i < results_array->Length(); ++i) {
v8::MaybeLocal<v8::Value> maybe_result = results_array->Get(context, i);
v8::Local<v8::Value> listener_return;
// Assume the result could throw due to changes at runtime by the
// extension's JS code.
if (!maybe_result.ToLocal(&listener_return)) {
continue;
}
// Check if any of the results is indicating it will reply async by
// returning `true`.
if (listener_return->IsBoolean() &&
listener_return.As<v8::Boolean>()->Value()) {
will_reply_async = true;
}
// If promise returns are not supported, then we don't need to attach any
// callbacks and can return early once we find at least one listener that
// wants to reply asynchronously
if (!IsMessagePolyfillSupportEnabled() && will_reply_async) {
return true;
}
// Check if any of the returns are a promise, indicating the listener will
// reply async. Attach callbacks for both the promise resolving or
// rejecting. This is so that whatever the promise settles to is considered
// the listener replying to the message sender with the settled value.
if (IsMessagePolyfillSupportEnabled() && listener_return->IsPromise()) {
auto promise_rejected_response_callback =
CreatePromiseRejectedCallback(port_id);
v8::Local<v8::Function> promise_rejected_function =
callback_manager_->CreateRespondingFunction(
script_context, port_id,
std::move(promise_rejected_response_callback));
std::ignore = listener_return.As<v8::Promise>()->Then(
context, promise_resolved_function, promise_rejected_function);
// TODO(crbug.com/40753031): Consider setting lastError for caller when
// promise is rejected.
will_reply_async = true;
}
}
return will_reply_async;
}
void OneTimeMessageHandler::OnEventFired(
const PortId& port_id,
std::optional<CallbackID> listener_error_callback_id,
gin::Arguments* arguments) {
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Value> result;
if (arguments->Length() > 0) {
CHECK(arguments->GetNext(&result));
} else {
result = v8::Undefined(isolate);
}
// The context could be tearing down by the time the event is fully
// dispatched.
OneTimeMessageContextData* data =
GetPerContextData<OneTimeMessageContextData>(context,
kDontCreateIfMissing);
if (!data)
return;
ScriptContext* script_context = GetScriptContextFromV8Context(context);
DCHECK(script_context)
<< "script context was destroyed before runtime.onMessage listener "
"results could be processed.";
// Cleanup listener error callback if created since it shouldn't be possible
// for synchronous thrown errors to appear after all listeners have finished
// being dispatched to.
if (IsMessagePolyfillSupportEnabled() && listener_error_callback_id) {
callback_manager_->DeleteCallbackDataForCallbackId(
script_context, port_id, *listener_error_callback_id);
}
auto iter = data->receivers.find(port_id);
// The channel may be closed (if the listener replied or threw an error).
if (iter == data->receivers.end()) {
return;
}
OneTimeReceiver& port = iter->second;
v8::Local<v8::Function> promise_resolved_function;
if (IsMessagePolyfillSupportEnabled()) {
promise_resolved_function = port.message_response_function.Get(isolate);
// Ensure the global function doesn't outlive port closing.
port.message_response_function.SetWeak();
}
if (CheckAndHandleAsyncListenerReply(isolate, context, *script_context,
result, port_id,
promise_resolved_function)) {
// Inform the browser that one of the listeners said they would be replying
// later and leave the channel open.
if (auto* message_port_host =
messaging_service()->GetMessagePortHostIfExists(script_context,
port_id)) {
message_port_host->ResponsePending();
}
return;
}
// The listener did not reply and did not indicate it would reply later from
// any of its listeners. Close the message port. Don't close the channel
// because another listener (in a separate context) may reply.
data->receivers.erase(port_id);
CloseReceiverMessagePortOrChannel(script_context, port_id,
/*close_channel=*/false,
/*error=*/std::nullopt);
}
// This must be defined in the .cc due to `OneTimeMessageHandler`'s header being
// included in `NativeExtensionBindingsSystem` causing a circular dependency.
NativeRendererMessagingService* OneTimeMessageHandler::messaging_service() {
return bindings_system_->messaging_service();
}
} // namespace extensions