123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- // Copyright 2017 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 "extensions/renderer/one_time_message_handler.h"
- #include <map>
- #include "base/bind.h"
- #include "base/callback.h"
- #include "base/containers/contains.h"
- #include "base/containers/cxx20_erase.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/mojom/event_dispatcher.mojom.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/gc_callback.h"
- #include "extensions/renderer/ipc_message_sender.h"
- #include "extensions/renderer/message_target.h"
- #include "extensions/renderer/messaging_util.h"
- #include "extensions/renderer/native_extension_bindings_system.h"
- #include "extensions/renderer/script_context.h"
- #include "gin/arguments.h"
- #include "gin/dictionary.h"
- #include "gin/handle.h"
- #include "gin/per_context_data.h"
- #include "ipc/ipc_message.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;
- int routing_id = MSG_ROUTING_NONE;
- binding::AsyncResponseType async_type = binding::AsyncResponseType::kNone;
- };
- // A receiver port in the context; i.e., a listener to runtime.onMessage.
- struct OneTimeReceiver {
- int routing_id = MSG_ROUTING_NONE;
- std::string event_name;
- v8::Global<v8::Object> sender;
- };
- using OneTimeMessageCallback =
- base::OnceCallback<void(gin::Arguments* arguments)>;
- struct OneTimeMessageContextData : public base::SupportsUserData::Data {
- static constexpr char kPerContextDataKey[] =
- "extension_one_time_message_context_data";
- std::map<PortId, OneTimeOpener> openers;
- std::map<PortId, OneTimeReceiver> receivers;
- std::vector<std::unique_ptr<OneTimeMessageCallback>> pending_callbacks;
- };
- constexpr char OneTimeMessageContextData::kPerContextDataKey[];
- int RoutingIdForScriptContext(ScriptContext* script_context) {
- content::RenderFrame* render_frame = script_context->GetRenderFrame();
- return render_frame ? render_frame->GetRoutingID() : MSG_ROUTING_NONE;
- }
- void OneTimeMessageResponseHelper(
- const v8::FunctionCallbackInfo<v8::Value>& info) {
- CHECK(info.Data()->IsExternal());
- 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;
- v8::Local<v8::External> external = info.Data().As<v8::External>();
- auto* raw_callback = static_cast<OneTimeMessageCallback*>(external->Value());
- auto iter = std::find_if(
- data->pending_callbacks.begin(), data->pending_callbacks.end(),
- [raw_callback](const std::unique_ptr<OneTimeMessageCallback>& callback) {
- return callback.get() == raw_callback;
- });
- if (iter == data->pending_callbacks.end())
- return;
- std::unique_ptr<OneTimeMessageCallback> callback = std::move(*iter);
- data->pending_callbacks.erase(iter);
- std::move(*callback).Run(&arguments);
- }
- // Called with the results of dispatching an onMessage event to listeners.
- // Returns true if any of the listeners responded with `true`, indicating they
- // will respond to the call asynchronously.
- bool WillListenerReplyAsync(v8::Local<v8::Context> context,
- v8::MaybeLocal<v8::Value> maybe_results) {
- v8::Local<v8::Value> results;
- // |maybe_results| can be empty if the context was destroyed before the
- // listeners were ran (or while they were running).
- if (!maybe_results.ToLocal(&results))
- return false;
- if (!results->IsObject())
- return false;
- // Suppress any script errors, but bail out if they happen (in theory, we
- // shouldn't have any).
- v8::Isolate* isolate = context->GetIsolate();
- v8::TryCatch try_catch(isolate);
- // We expect results in the form of an object with an array of results as
- // a `results` property.
- v8::Local<v8::Value> results_property;
- if (!results.As<v8::Object>()
- ->Get(context, gin::StringToSymbol(isolate, "results"))
- .ToLocal(&results_property) ||
- !results_property->IsArray()) {
- return false;
- }
- // Check if any of the results is `true`.
- v8::Local<v8::Array> array = results_property.As<v8::Array>();
- uint32_t length = array->Length();
- for (uint32_t i = 0; i < length; ++i) {
- v8::Local<v8::Value> val;
- if (!array->Get(context, i).ToLocal(&val))
- return false;
- if (val->IsTrue())
- return true;
- }
- return false;
- }
- } // namespace
- OneTimeMessageHandler::OneTimeMessageHandler(
- NativeExtensionBindingsSystem* bindings_system)
- : bindings_system_(bindings_system) {}
- OneTimeMessageHandler::~OneTimeMessageHandler() {}
- 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,
- const std::string& method_name,
- const Message& message,
- binding::AsyncResponseType async_type,
- v8::Local<v8::Function> response_callback) {
- 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;
- int routing_id = RoutingIdForScriptContext(script_context);
- 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.routing_id = routing_id;
- port.async_type = async_type;
- promise = details.promise;
- DCHECK_EQ(async_type == binding::AsyncResponseType::kPromise,
- !promise.IsEmpty());
- }
- IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
- ipc_sender->SendOpenMessageChannel(script_context, new_port_id, target,
- method_name);
- ipc_sender->SendPostMessageToPort(new_port_id, 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) {
- bool close_channel = true;
- ipc_sender->SendCloseMessagePort(routing_id, new_port_id, close_channel);
- }
- 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.routing_id = RoutingIdForScriptContext(script_context);
- receiver.event_name = event_name;
- }
- 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) {
- v8::Isolate* isolate = script_context->isolate();
- v8::HandleScope handle_scope(isolate);
- OneTimeMessageContextData* data =
- GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
- kDontCreateIfMissing);
- return data ? data->pending_callbacks.size() : 0;
- }
- 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.
- // TODO(devlin): With chrome.runtime.sendMessage, we actually require that a
- // listener return `true` if they intend to respond asynchronously; otherwise
- // we close the port.
- auto callback = std::make_unique<OneTimeMessageCallback>(
- base::BindOnce(&OneTimeMessageHandler::OnOneTimeMessageResponse,
- weak_factory_.GetWeakPtr(), target_port_id));
- v8::Local<v8::External> external = v8::External::New(isolate, callback.get());
- v8::Local<v8::Function> response_function;
- if (!v8::Function::New(context, &OneTimeMessageResponseHelper, external)
- .ToLocal(&response_function)) {
- NOTREACHED();
- return handled;
- }
- new GCCallback(
- script_context, response_function,
- base::BindOnce(&OneTimeMessageHandler::OnResponseCallbackCollected,
- weak_factory_.GetWeakPtr(), script_context, target_port_id,
- callback.get()),
- base::OnceClosure());
- v8::HandleScope handle_scope(isolate);
- v8::Local<v8::Value> v8_message =
- messaging_util::MessageToV8(context, message);
- v8::Local<v8::Object> v8_sender = port.sender.Get(isolate);
- std::vector<v8::Local<v8::Value>> args = {v8_message, v8_sender,
- response_function};
- JSRunner::ResultCallback dispatch_callback;
- // For runtime.onMessage, we require that the listener return `true` if they
- // intend to respond asynchronously. Check the results of the listeners.
- if (port.event_name == messaging_util::kOnMessageEvent) {
- dispatch_callback =
- base::BindOnce(&OneTimeMessageHandler::OnEventFired,
- weak_factory_.GetWeakPtr(), target_port_id);
- }
- data->pending_callbacks.push_back(std::move(callback));
- bindings_system_->api_system()->event_handler()->FireEventInContext(
- port.event_name, context, &args, nullptr, std::move(dispatch_callback));
- // 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::Local<v8::Value> v8_message =
- messaging_util::MessageToV8(v8_context, message);
- std::vector<v8::Local<v8::Value>> args = {v8_message};
- bindings_system_->api_system()->request_handler()->CompleteRequest(
- port.request_id, args, std::string());
- bool close_channel = true;
- bindings_system_->GetIPCMessageSender()->SendCloseMessagePort(
- port.routing_id, target_port_id, close_channel);
- // 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;
- data->receivers.erase(iter);
- 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, std::vector<v8::Local<v8::Value>>(), error);
- // Note: The context could be invalidated at this point!
- return handled;
- }
- 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 return `true`), it might be good to surface an
- // error.
- OneTimeMessageContextData* data =
- GetPerContextData<OneTimeMessageContextData>(context,
- kDontCreateIfMissing);
- if (!data)
- return;
- auto iter = data->receivers.find(port_id);
- if (iter == data->receivers.end())
- return;
- int routing_id = iter->second.routing_id;
- data->receivers.erase(iter);
- 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);
- std::string error;
- std::unique_ptr<Message> message = messaging_util::MessageFromV8(
- context, value, port_id.serialization_format, &error);
- if (!message) {
- arguments->ThrowTypeError(error);
- return;
- }
- IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
- ipc_sender->SendPostMessageToPort(port_id, *message);
- bool close_channel = true;
- ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
- }
- void OneTimeMessageHandler::OnResponseCallbackCollected(
- ScriptContext* script_context,
- const PortId& port_id,
- void* raw_callback) {
- // 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;
- 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;
- int routing_id = iter->second.routing_id;
- data->receivers.erase(iter);
- // Since there is no way to call the callback anymore, we can remove it from
- // the pending callbacks.
- base::EraseIf(
- data->pending_callbacks,
- [raw_callback](const std::unique_ptr<OneTimeMessageCallback>& callback) {
- return callback.get() == raw_callback;
- });
- // Close the message port. There's no way to send a reply anymore. Don't
- // close the channel because another listener may reply.
- IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
- bool close_channel = false;
- ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
- }
- void OneTimeMessageHandler::OnEventFired(const PortId& port_id,
- v8::Local<v8::Context> context,
- v8::MaybeLocal<v8::Value> result) {
- // The context could be tearing down by the time the event is fully
- // dispatched.
- OneTimeMessageContextData* data =
- GetPerContextData<OneTimeMessageContextData>(context,
- kDontCreateIfMissing);
- if (!data)
- return;
- auto iter = data->receivers.find(port_id);
- // The channel may already be closed (if the listener replied).
- if (iter == data->receivers.end())
- return;
- int routing_id = iter->second.routing_id;
- IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
- if (WillListenerReplyAsync(context, result)) {
- // Inform the browser that one of the listeners said they would be replying
- // later and leave the channel open.
- ipc_sender->SendMessageResponsePending(routing_id, port_id);
- return;
- }
- data->receivers.erase(iter);
- // The listener did not reply and did not return `true` from any of its
- // listeners. Close the message port. Don't close the channel because another
- // listener (in a separate context) may reply.
- bool close_channel = false;
- ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
- }
- } // namespace extensions
|