blob: 66bae6b997ae0376d4a695e51103e76efa860b24 [file] [log] [blame]
// 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 <memory>
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_bindings_system.h"
#include "extensions/renderer/bindings/api_request_handler.h"
#include "extensions/renderer/message_target.h"
#include "extensions/renderer/messaging_util.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/native_extension_bindings_system_test_base.h"
#include "extensions/renderer/script_context.h"
#include "gin/data_object_builder.h"
#include "ipc/ipc_message.h"
namespace extensions {
namespace {
constexpr char kEchoArgsAndError[] =
"(function() {\n"
" this.replyArgs = Array.from(arguments);\n"
" this.lastError =\n"
" chrome.runtime.lastError ?\n"
" chrome.runtime.lastError.message : undefined;\n"
"})";
} // namespace
class OneTimeMessageHandlerTest : public NativeExtensionBindingsSystemUnittest {
public:
OneTimeMessageHandlerTest() {}
~OneTimeMessageHandlerTest() override {}
void SetUp() override {
NativeExtensionBindingsSystemUnittest::SetUp();
message_handler_ =
std::make_unique<OneTimeMessageHandler>(bindings_system());
extension_ = ExtensionBuilder("foo").Build();
RegisterExtension(extension_);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
script_context_ = CreateScriptContext(context, extension_.get(),
Feature::BLESSED_EXTENSION_CONTEXT);
script_context_->set_url(extension_->url());
bindings_system()->UpdateBindingsForContext(script_context_);
}
void TearDown() override {
script_context_ = nullptr;
extension_ = nullptr;
message_handler_.reset();
NativeExtensionBindingsSystemUnittest::TearDown();
}
bool UseStrictIPCMessageSender() override { return true; }
std::string GetGlobalProperty(v8::Local<v8::Context> context,
base::StringPiece property) {
return GetStringPropertyFromObject(context->Global(), context, property);
}
OneTimeMessageHandler* message_handler() { return message_handler_.get(); }
ScriptContext* script_context() { return script_context_; }
const Extension* extension() { return extension_.get(); }
private:
std::unique_ptr<OneTimeMessageHandler> message_handler_;
ScriptContext* script_context_ = nullptr;
scoped_refptr<const Extension> extension_;
DISALLOW_COPY_AND_ASSIGN(OneTimeMessageHandlerTest);
};
// Tests sending a message without expecting a reply, as in
// chrome.runtime.sendMessage({foo: 'bar'});
TEST_F(OneTimeMessageHandlerTest, SendMessageAndDontExpectReply) {
const PortId port_id(script_context()->context_id(), 0, true);
const bool include_tls_channel_id = false;
const Message message("\"Hello\"", false);
// We should open a message port, send a message, and then close it
// immediately.
MessageTarget target(MessageTarget::ForExtension(extension()->id()));
EXPECT_CALL(*ipc_message_sender(),
SendOpenMessageChannel(script_context(), port_id, target,
messaging_util::kSendMessageChannel,
include_tls_channel_id));
EXPECT_CALL(*ipc_message_sender(), SendPostMessageToPort(port_id, message));
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, true));
message_handler()->SendMessage(
script_context(), port_id, target, messaging_util::kSendMessageChannel,
include_tls_channel_id, message, v8::Local<v8::Function>());
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
}
// Tests sending a message and expecting a reply, as in
// chrome.runtime.sendMessage({foo: 'bar'}, function(reply) { ... });
TEST_F(OneTimeMessageHandlerTest, SendMessageAndExpectReply) {
const PortId port_id(script_context()->context_id(), 0, true);
const bool include_tls_channel_id = false;
const Message message("\"Hello\"", false);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Function> callback =
FunctionFromString(context, kEchoArgsAndError);
APIRequestHandler* request_handler =
bindings_system()->api_system()->request_handler();
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
// We should open a message port and send a message, and the message port
// should remain open (to allow for a reply).
MessageTarget target(MessageTarget::ForExtension(extension()->id()));
EXPECT_CALL(*ipc_message_sender(),
SendOpenMessageChannel(script_context(), port_id, target,
messaging_util::kSendMessageChannel,
include_tls_channel_id));
EXPECT_CALL(*ipc_message_sender(), SendPostMessageToPort(port_id, message));
message_handler()->SendMessage(script_context(), port_id, target,
messaging_util::kSendMessageChannel,
include_tls_channel_id, message, callback);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
// We should have added a pending request to the APIRequestHandler, but
// shouldn't yet have triggered the reply callback.
EXPECT_FALSE(request_handler->GetPendingRequestIdsForTesting().empty());
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
EXPECT_EQ("undefined", GetGlobalProperty(context, "replyArgs"));
EXPECT_EQ("undefined", GetGlobalProperty(context, "lastError"));
// Deliver the reply; the message port should close.
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, true));
const Message reply("\"Hi\"", false);
message_handler()->DeliverMessage(script_context(), reply, port_id);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
// And the callback should have been triggered, completing the request.
EXPECT_EQ("[\"Hi\"]", GetGlobalProperty(context, "replyArgs"));
EXPECT_EQ("undefined", GetGlobalProperty(context, "lastError"));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
}
// Tests disconnecting an opener (initiator of a sendMessage() call); this can
// happen when no receiving end exists (i.e., no listener to runtime.onMessage).
TEST_F(OneTimeMessageHandlerTest, DisconnectOpener) {
const PortId port_id(script_context()->context_id(), 0, true);
const bool include_tls_channel_id = false;
const Message message("\"Hello\"", false);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Function> callback =
FunctionFromString(context, kEchoArgsAndError);
MessageTarget target(MessageTarget::ForExtension(extension()->id()));
EXPECT_CALL(*ipc_message_sender(),
SendOpenMessageChannel(script_context(), port_id, target,
messaging_util::kSendMessageChannel,
include_tls_channel_id));
EXPECT_CALL(*ipc_message_sender(), SendPostMessageToPort(port_id, message));
message_handler()->SendMessage(script_context(), port_id, target,
messaging_util::kSendMessageChannel,
include_tls_channel_id, message, callback);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_EQ("undefined", GetGlobalProperty(context, "replyArgs"));
EXPECT_EQ("undefined", GetGlobalProperty(context, "lastError"));
// Disconnect the opener with an error. The callback should be triggered, and
// the port should be removed. chrome.runtime.lastError should have been
// populated.
message_handler()->Disconnect(script_context(), port_id, "No receiving end");
EXPECT_EQ("[]", GetGlobalProperty(context, "replyArgs"));
EXPECT_EQ("\"No receiving end\"", GetGlobalProperty(context, "lastError"));
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
}
// Tests delivering a message to a receiver and not replying, as in
// chrome.runtime.onMessage.addListener(function(message, sender, reply) {
// ...
// });
TEST_F(OneTimeMessageHandlerTest, DeliverMessageToReceiverWithNoReply) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kRegisterListener[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(\n"
" function(message, sender, reply) {\n"
" this.eventMessage = message;\n"
" this.eventSender = sender;\n"
" return true; // Reply later\n"
" });\n"
"})";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kRegisterListener);
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
EXPECT_EQ("undefined", GetGlobalProperty(context, "eventMessage"));
EXPECT_EQ("undefined", GetGlobalProperty(context, "eventSender"));
base::UnguessableToken other_context_id = base::UnguessableToken::Create();
const PortId port_id(other_context_id, 0, false);
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
v8::Local<v8::Object> sender = gin::DataObjectBuilder(isolate())
.Set("key", std::string("sender"))
.Build();
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
EXPECT_EQ("undefined", GetGlobalProperty(context, "eventMessage"));
EXPECT_EQ("undefined", GetGlobalProperty(context, "eventSender"));
const Message message("\"Hi\"", false);
message_handler()->DeliverMessage(script_context(), message, port_id);
EXPECT_EQ("\"Hi\"", GetGlobalProperty(context, "eventMessage"));
EXPECT_EQ(R"({"key":"sender"})", GetGlobalProperty(context, "eventSender"));
// TODO(devlin): Right now, the port lives eternally. In JS bindings, we have
// two ways of dealing with this:
// - monitoring the lifetime of the reply object
// - requiring the extension to return true from an onMessage handler
// We should implement these and test lifetime.
}
// Tests delivering a message to a receiver and replying, as in
// chrome.runtime.onMessage.addListener(function(message, sender, reply) {
// reply('foo');
// });
TEST_F(OneTimeMessageHandlerTest, DeliverMessageToReceiverAndReply) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kRegisterListener[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(\n"
" function(message, sender, reply) {\n"
" reply({data: 'hey'});\n"
" });\n"
"})";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kRegisterListener);
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
base::UnguessableToken other_context_id = base::UnguessableToken::Create();
const PortId port_id(other_context_id, 0, false);
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
v8::Local<v8::Object> sender = v8::Object::New(isolate());
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
const Message message("\"Hi\"", false);
// When the listener replies, we should post the reply to the message port and
// close the channel.
EXPECT_CALL(
*ipc_message_sender(),
SendPostMessageToPort(port_id, Message(R"({"data":"hey"})", false)));
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, true));
message_handler()->DeliverMessage(script_context(), message, port_id);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
}
// Tests that nothing breaks when trying to call the reply callback multiple
// times.
TEST_F(OneTimeMessageHandlerTest, TryReplyingMultipleTimes) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kRegisterListener[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(\n"
" function(message, sender, reply) {\n"
" this.sendReply = reply;\n"
" return true; // Reply later\n"
" });\n"
"})";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kRegisterListener);
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
base::UnguessableToken other_context_id = base::UnguessableToken::Create();
const PortId port_id(other_context_id, 0, false);
v8::Local<v8::Object> sender = v8::Object::New(isolate());
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
const Message message("\"Hi\"", false);
message_handler()->DeliverMessage(script_context(), message, port_id);
v8::Local<v8::Value> reply =
GetPropertyFromObject(context->Global(), context, "sendReply");
ASSERT_FALSE(reply.IsEmpty());
ASSERT_TRUE(reply->IsFunction());
v8::Local<v8::Value> reply_arg = V8ValueFromScriptSource(context, "'hi'");
v8::Local<v8::Value> args[] = {reply_arg};
EXPECT_CALL(*ipc_message_sender(),
SendPostMessageToPort(port_id, Message("\"hi\"", false)));
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, true));
RunFunction(reply.As<v8::Function>(), context, base::size(args), args);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
// Running the reply function a second time shouldn't do anything.
// TODO(devlin): Add an error message.
RunFunction(reply.As<v8::Function>(), context, base::size(args), args);
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
}
// Test starting a new sendMessage call from a sendMessage listener.
TEST_F(OneTimeMessageHandlerTest, SendMessageInListener) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kRegisterListener[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(\n"
" function(message, sender, reply) {\n"
" chrome.runtime.sendMessage('foo', function() {});\n"
" });\n"
"})";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kRegisterListener);
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
base::UnguessableToken sender_context_id = base::UnguessableToken::Create();
const PortId original_port_id(sender_context_id, 0, false);
v8::Local<v8::Object> sender = v8::Object::New(isolate());
message_handler()->AddReceiver(script_context(), original_port_id, sender,
messaging_util::kOnMessageEvent);
// On delivering the message, we expect the listener to open a new message
// channel by using sendMessage(). The original message channel will be
// closed.
const PortId listener_created_port_id(script_context()->context_id(), 0,
true);
const Message listener_sent_message("\"foo\"", false);
MessageTarget target(MessageTarget::ForExtension(extension()->id()));
EXPECT_CALL(
*ipc_message_sender(),
SendOpenMessageChannel(script_context(), listener_created_port_id, target,
messaging_util::kSendMessageChannel, false));
EXPECT_CALL(
*ipc_message_sender(),
SendPostMessageToPort(listener_created_port_id, listener_sent_message));
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, original_port_id, false));
const Message message("\"Hi\"", false);
message_handler()->DeliverMessage(script_context(), message,
original_port_id);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
}
// Test using sendMessage from the reply to a sendMessage call.
TEST_F(OneTimeMessageHandlerTest, SendMessageInCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kSendMessage[] =
"(function() {\n"
" chrome.runtime.sendMessage(\n"
" 'foo',\n"
" function(reply) {\n"
" chrome.runtime.sendMessage('bar', function() {});\n"
" });\n"
"})";
v8::Local<v8::Function> send_message =
FunctionFromString(context, kSendMessage);
// Running the function should send one message ('foo'), which will wait for
// a reply.
const PortId original_port_id(script_context()->context_id(), 0, true);
const Message original_message("\"foo\"", false);
MessageTarget target(MessageTarget::ForExtension(extension()->id()));
EXPECT_CALL(
*ipc_message_sender(),
SendOpenMessageChannel(script_context(), original_port_id, target,
messaging_util::kSendMessageChannel, false));
EXPECT_CALL(*ipc_message_sender(),
SendPostMessageToPort(original_port_id, original_message));
RunFunctionOnGlobal(send_message, context, 0, nullptr);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
// Upon delivering the reply to the sender, it should send a second message
// ('bar'). The original message channel should be closed.
const PortId new_port_id(script_context()->context_id(), 1, true);
EXPECT_CALL(
*ipc_message_sender(),
SendOpenMessageChannel(script_context(), new_port_id, target,
messaging_util::kSendMessageChannel, false));
EXPECT_CALL(*ipc_message_sender(),
SendPostMessageToPort(new_port_id, Message("\"bar\"", false)));
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, original_port_id, true));
const Message reply("\"reply\"", false);
message_handler()->DeliverMessage(script_context(), reply, original_port_id);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
}
TEST_F(OneTimeMessageHandlerTest, ResponseCallbackGarbageCollected) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
constexpr char kRegisterListener[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(\n"
" function(message, sender, reply) {\n"
" return true; // Reply later\n"
" });\n"
"})";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kRegisterListener);
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
base::UnguessableToken other_context_id = base::UnguessableToken::Create();
const PortId port_id(other_context_id, 0, false);
v8::Local<v8::Object> sender = v8::Object::New(isolate());
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
const Message message("\"Hi\"", false);
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, false));
message_handler()->DeliverMessage(script_context(), message, port_id);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
// The listener didn't retain the reply callback, so it should be garbage
// collected.
RunGarbageCollection();
base::RunLoop().RunUntilIdle();
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
}
// runtime.onMessage requires that a listener return `true` if they intend to
// respond to the message asynchronously. Verify that we close the port if no
// listener does so.
TEST_F(OneTimeMessageHandlerTest, ChannelClosedIfTrueNotReturned) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
auto register_listener = [context](const char* listener) {
constexpr char kRegisterListenerTemplate[] =
"(function() { chrome.runtime.onMessage.addListener(%s); })";
v8::Local<v8::Function> add_listener = FunctionFromString(
context, base::StringPrintf(kRegisterListenerTemplate, listener));
RunFunctionOnGlobal(add_listener, context, 0, nullptr);
};
register_listener("function(message, reply, sender) { }");
// Add a listener that returns a truthy value, but not `true`.
register_listener("function(message, reply, sender) { return {}; }");
// Add a listener that throws an error.
register_listener(
"function(message, reply, sender) { throw new Error('hi!'); }");
base::UnguessableToken other_context_id = base::UnguessableToken::Create();
const PortId port_id(other_context_id, 0, false);
v8::Local<v8::Object> sender = v8::Object::New(isolate());
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
TestJSRunner::AllowErrors allow_errors;
// Dispatch the message. Since none of these listeners return `true`, the port
// should close.
const Message message("\"Hi\"", false);
EXPECT_CALL(*ipc_message_sender(),
SendCloseMessagePort(MSG_ROUTING_NONE, port_id, false));
message_handler()->DeliverMessage(script_context(), message, port_id);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
// If any of the listeners return `true`, the channel should be left open.
register_listener("function(message, reply, sender) { return true; }");
message_handler()->AddReceiver(script_context(), port_id, sender,
messaging_util::kOnMessageEvent);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
message_handler()->DeliverMessage(script_context(), message, port_id);
EXPECT_TRUE(message_handler()->HasPort(script_context(), port_id));
}
} // namespace extensions