| // 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/native_renderer_messaging_service.h" |
| |
| #include <memory> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/values.h" |
| #include "components/crx_file/id_util.h" |
| #include "content/public/common/content_constants.h" |
| #include "extensions/common/api/messaging/messaging_endpoint.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/mojom/context_type.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/api/messaging/mock_message_port_host.h" |
| #include "extensions/renderer/bindings/api_binding_test_util.h" |
| #include "extensions/renderer/bindings/api_binding_types.h" |
| #include "extensions/renderer/bindings/api_response_validator.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 "extensions/renderer/script_context_set.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Unfortunately, we have a layering violation in the runtime API. The runtime |
| // API is defined at the //extensions layer, but the `MessageSender` type has an |
| // optional `tabs.Tab` property. This causes issues in type validation because |
| // when we try to look up the `tabs.Tab` property, it fails (since it's only |
| // defined in //chrome). This is a "real bug" in that it's a layering violation, |
| // but it doesn't have real-world implications since right now the only consumer |
| // of //extensions is //chrome (and thus, the tabs API will always be defined). |
| // Ignore validation for the affected runtime message-related events. |
| class RuntimeMessageValidationIgnorer { |
| public: |
| RuntimeMessageValidationIgnorer() |
| : test_handler_(base::BindRepeating( |
| &RuntimeMessageValidationIgnorer::HardValidationFailure)) { |
| test_handler_.IgnoreSignature("runtime.onMessage"); |
| test_handler_.IgnoreSignature("runtime.onMessageExternal"); |
| test_handler_.IgnoreSignature("runtime.onConnect"); |
| } |
| ~RuntimeMessageValidationIgnorer() = default; |
| |
| private: |
| // Hard-fail on any unexpected validation errors. |
| static void HardValidationFailure(const std::string& name, |
| const std::string& failure) { |
| NOTREACHED() << "Unexpected validation failure: " << name << ", " |
| << failure; |
| } |
| |
| APIResponseValidator::TestHandler test_handler_; |
| }; |
| |
| } // namespace |
| |
| class NativeRendererMessagingServiceTest |
| : public NativeExtensionBindingsSystemUnittest { |
| public: |
| NativeRendererMessagingServiceTest() {} |
| |
| NativeRendererMessagingServiceTest( |
| const NativeRendererMessagingServiceTest&) = delete; |
| NativeRendererMessagingServiceTest& operator=( |
| const NativeRendererMessagingServiceTest&) = delete; |
| |
| ~NativeRendererMessagingServiceTest() override {} |
| |
| // NativeExtensionBindingsSystemUnittest: |
| void SetUp() override { |
| NativeExtensionBindingsSystemUnittest::SetUp(); |
| |
| extension_ = ExtensionBuilder("foo").Build(); |
| RegisterExtension(extension_); |
| |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| script_context_ = CreateScriptContext( |
| context, extension_.get(), mojom::ContextType::kPrivilegedExtension); |
| script_context_->set_url(extension_->url()); |
| bindings_system()->UpdateBindingsForContext(script_context_); |
| } |
| void TearDown() override { |
| script_context_ = nullptr; |
| extension_ = nullptr; |
| NativeExtensionBindingsSystemUnittest::TearDown(); |
| } |
| bool UseStrictIPCMessageSender() override { return true; } |
| |
| NativeRendererMessagingService* messaging_service() { |
| return bindings_system()->messaging_service(); |
| } |
| ScriptContext* script_context() { return script_context_; } |
| const Extension* extension() { return extension_.get(); } |
| |
| private: |
| raw_ptr<ScriptContext> script_context_ = nullptr; |
| scoped_refptr<const Extension> extension_; |
| }; |
| |
| TEST_F(NativeRendererMessagingServiceTest, OpenMessagePort) { |
| RuntimeMessageValidationIgnorer message_validation_ignorer; |
| |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| EXPECT_FALSE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| |
| const std::string channel_name = "some channel"; |
| NativeRendererMessagingService::TabConnectionInfo tab_connection_info; |
| NativeRendererMessagingService::ExternalConnectionInfo |
| external_connection_info; |
| tab_connection_info.frame_id = 0; |
| const int tab_id = 10; |
| GURL source_url("http://example.com"); |
| tab_connection_info.tab = base::Value::Dict().Set("tabId", tab_id); |
| external_connection_info.target_id = extension()->id(); |
| external_connection_info.source_endpoint = |
| MessagingEndpoint::ForExtension(extension()->id()); |
| external_connection_info.source_url = source_url; |
| external_connection_info.guest_process_id = |
| content::kInvalidChildProcessUniqueId; |
| external_connection_info.guest_render_frame_routing_id = 0; |
| |
| const char kAddListener[] = |
| "(function() {\n" |
| " chrome.runtime.onConnect.addListener(function(port) {\n" |
| " this.eventFired = true;\n" |
| " this.sender = port.sender\n" |
| " });\n" |
| "})"; |
| v8::Local<v8::Function> add_listener = |
| FunctionFromString(context, kAddListener); |
| RunFunctionOnGlobal(add_listener, context, 0, nullptr); |
| |
| mojo::PendingAssociatedRemote<mojom::MessagePortHost> port_host_remote; |
| auto port_host_receiver = |
| port_host_remote.InitWithNewEndpointAndPassReceiver(); |
| |
| mojo::PendingAssociatedReceiver<mojom::MessagePort> port_receiver; |
| auto port_remote = port_receiver.InitWithNewEndpointAndPassRemote(); |
| |
| bool port_opened = false; |
| messaging_service()->DispatchOnConnect( |
| script_context_set(), port_id, mojom::ChannelType::kConnect, channel_name, |
| tab_connection_info, external_connection_info, std::move(port_receiver), |
| std::move(port_host_remote), nullptr, |
| base::BindLambdaForTesting( |
| [&port_opened](bool success) { port_opened = success; })); |
| port_host_receiver.EnableUnassociatedUsage(); |
| port_remote.EnableUnassociatedUsage(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| EXPECT_TRUE(port_opened); |
| |
| ASSERT_TRUE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| |
| EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context, |
| "eventFired")); |
| base::Value::Dict expected_sender = |
| base::Value::Dict() |
| .Set("frameId", 0) |
| .Set("tab", base::Value::Dict().Set("tabId", tab_id)) |
| .Set("url", source_url.spec()) |
| .Set("id", extension()->id()); |
| EXPECT_EQ(ValueToString(base::Value(std::move(expected_sender))), |
| GetStringPropertyFromObject(context->Global(), context, "sender")); |
| } |
| |
| TEST_F(NativeRendererMessagingServiceTest, DeliverMessageToPort) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id1(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| const PortId port_id2(other_context_id, 1, false, |
| mojom::SerializationFormat::kJson); |
| |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote1; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver1; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote2; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver2; |
| |
| GinPort* port1 = messaging_service()->CreatePortForTesting( |
| script_context(), "channel1", mojom::ChannelType::kSendMessage, port_id1, |
| message_port_remote1, message_port_host_receiver1); |
| GinPort* port2 = messaging_service()->CreatePortForTesting( |
| script_context(), "channel2", mojom::ChannelType::kSendMessage, port_id2, |
| message_port_remote2, message_port_host_receiver2); |
| message_port_remote1.EnableUnassociatedUsage(); |
| message_port_host_receiver1.EnableUnassociatedUsage(); |
| message_port_remote2.EnableUnassociatedUsage(); |
| message_port_host_receiver2.EnableUnassociatedUsage(); |
| |
| const char kOnMessageListenerTemplate[] = |
| "(function(port) {\n" |
| " port.onMessage.addListener((message) => {\n" |
| " this.%s = message;\n" |
| " });\n" |
| "})"; |
| const char kPort1Message[] = "port1Message"; |
| const char kPort2Message[] = "port2Message"; |
| { |
| v8::Local<v8::Function> add_on_message_listener = FunctionFromString( |
| context, base::StringPrintf(kOnMessageListenerTemplate, kPort1Message)); |
| v8::Local<v8::Value> args[] = { |
| port1->GetWrapper(isolate()).ToLocalChecked()}; |
| RunFunctionOnGlobal(add_on_message_listener, context, std::size(args), |
| args); |
| } |
| { |
| v8::Local<v8::Function> add_on_message_listener = FunctionFromString( |
| context, base::StringPrintf(kOnMessageListenerTemplate, kPort2Message)); |
| v8::Local<v8::Value> args[] = { |
| port2->GetWrapper(isolate()).ToLocalChecked()}; |
| RunFunctionOnGlobal(add_on_message_listener, context, std::size(args), |
| args); |
| } |
| |
| // We've only added listeners (not dispatched any messages), so neither |
| // listener should have been triggered. |
| v8::Local<v8::Object> global = context->Global(); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort1Message)); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort2Message)); |
| |
| const char kMessageString[] = R"({"data":"hello"})"; |
| messaging_service()->DeliverMessage( |
| script_context_set(), port_id1, |
| Message(kMessageString, mojom::SerializationFormat::kJson, false), |
| nullptr); |
| |
| // Only port1 should have been notified of the message (ports only receive |
| // messages directed to themselves). |
| EXPECT_EQ(kMessageString, |
| GetStringPropertyFromObject(global, context, kPort1Message)); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort2Message)); |
| } |
| |
| TEST_F(NativeRendererMessagingServiceTest, DisconnectMessagePort) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id1(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| const PortId port_id2(other_context_id, 1, false, |
| mojom::SerializationFormat::kJson); |
| |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote1; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver1; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote2; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver2; |
| GinPort* port1 = messaging_service()->CreatePortForTesting( |
| script_context(), "channel1", mojom::ChannelType::kSendMessage, port_id1, |
| message_port_remote1, message_port_host_receiver1); |
| GinPort* port2 = messaging_service()->CreatePortForTesting( |
| script_context(), "channel2", mojom::ChannelType::kSendMessage, port_id2, |
| message_port_remote2, message_port_host_receiver2); |
| message_port_remote1.EnableUnassociatedUsage(); |
| message_port_host_receiver1.EnableUnassociatedUsage(); |
| message_port_remote2.EnableUnassociatedUsage(); |
| message_port_host_receiver2.EnableUnassociatedUsage(); |
| |
| const char kOnDisconnectListenerTemplate[] = |
| "(function(port) {\n" |
| " port.onDisconnect.addListener(() => {\n" |
| " this.%s = true;\n" |
| " });\n" |
| "})"; |
| const char kPort1Disconnect[] = "port1Disconnect"; |
| const char kPort2Disconnect[] = "port2Disconnect"; |
| { |
| v8::Local<v8::Function> add_on_disconnect_listener = FunctionFromString( |
| context, |
| base::StringPrintf(kOnDisconnectListenerTemplate, kPort1Disconnect)); |
| v8::Local<v8::Value> args[] = {port1->GetWrapper(isolate()).ToLocalChecked()}; |
| RunFunctionOnGlobal(add_on_disconnect_listener, context, std::size(args), |
| args); |
| } |
| { |
| v8::Local<v8::Function> add_on_disconnect_listener = FunctionFromString( |
| context, |
| base::StringPrintf(kOnDisconnectListenerTemplate, kPort2Disconnect)); |
| v8::Local<v8::Value> args[] = {port2->GetWrapper(isolate()).ToLocalChecked()}; |
| RunFunctionOnGlobal(add_on_disconnect_listener, context, std::size(args), |
| args); |
| } |
| |
| v8::Local<v8::Object> global = context->Global(); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort1Disconnect)); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort2Disconnect)); |
| |
| messaging_service()->DispatchOnDisconnect(script_context_set(), port_id1, |
| std::string(), nullptr); |
| |
| EXPECT_EQ("true", |
| GetStringPropertyFromObject(global, context, kPort1Disconnect)); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(global, context, kPort2Disconnect)); |
| } |
| |
| TEST_F(NativeRendererMessagingServiceTest, PostMessageFromJS) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| |
| MockMessagePortHost mock_message_port_host; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver; |
| GinPort* port = messaging_service()->CreatePortForTesting( |
| script_context(), "channel", mojom::ChannelType::kSendMessage, port_id, |
| message_port_remote, message_port_host_receiver); |
| message_port_remote.EnableUnassociatedUsage(); |
| message_port_host_receiver.EnableUnassociatedUsage(); |
| mock_message_port_host.BindReceiver(std::move(message_port_host_receiver)); |
| v8::Local<v8::Object> port_object = port->GetWrapper(isolate()).ToLocalChecked(); |
| |
| const char kDispatchMessage[] = |
| "(function(port) {\n" |
| " port.postMessage({data: 'hello'});\n" |
| "})"; |
| v8::Local<v8::Function> post_message = |
| FunctionFromString(context, kDispatchMessage); |
| v8::Local<v8::Value> args[] = {port_object}; |
| |
| base::RunLoop run_loop; |
| EXPECT_CALL(mock_message_port_host, |
| PostMessage(Message(R"({"data":"hello"})", |
| mojom::SerializationFormat::kJson, false))) |
| .WillOnce(base::test::RunClosure(run_loop.QuitClosure())); |
| RunFunctionOnGlobal(post_message, context, std::size(args), args); |
| run_loop.Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| } |
| |
| TEST_F(NativeRendererMessagingServiceTest, DisconnectFromJS) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| |
| MockMessagePortHost mock_message_port_host; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port_remote; |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver; |
| GinPort* port = messaging_service()->CreatePortForTesting( |
| script_context(), "channel", mojom::ChannelType::kSendMessage, port_id, |
| message_port_remote, message_port_host_receiver); |
| message_port_remote.EnableUnassociatedUsage(); |
| message_port_host_receiver.EnableUnassociatedUsage(); |
| mock_message_port_host.BindReceiver(std::move(message_port_host_receiver)); |
| v8::Local<v8::Object> port_object = port->GetWrapper(isolate()).ToLocalChecked(); |
| |
| const char kDispatchMessage[] = |
| "(function(port) {\n" |
| " port.disconnect();\n" |
| "})"; |
| v8::Local<v8::Function> post_message = |
| FunctionFromString(context, kDispatchMessage); |
| v8::Local<v8::Value> args[] = {port_object}; |
| |
| base::RunLoop run_loop; |
| EXPECT_CALL(mock_message_port_host, |
| ClosePort( |
| /*close_channel=*/true, |
| /*error_message=*/testing::Eq(std::nullopt))) |
| .WillOnce(base::test::RunClosure(run_loop.QuitClosure())); |
| RunFunctionOnGlobal(post_message, context, std::size(args), args); |
| run_loop.Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| } |
| |
| TEST_F(NativeRendererMessagingServiceTest, Connect) { |
| v8::HandleScope handle_scope(isolate()); |
| |
| const std::string kChannel = "channel"; |
| PortId expected_port_id(script_context()->context_id(), 0, true, |
| mojom::SerializationFormat::kJson); |
| MessageTarget target(MessageTarget::ForExtension(extension()->id())); |
| EXPECT_CALL(*ipc_message_sender(), |
| SendOpenMessageChannel(script_context(), expected_port_id, target, |
| mojom::ChannelType::kConnect, kChannel, |
| testing::_, testing::_)); |
| GinPort* new_port = messaging_service()->Connect( |
| script_context(), target, "channel", mojom::SerializationFormat::kJson); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ASSERT_TRUE(new_port); |
| |
| EXPECT_EQ(expected_port_id, new_port->port_id()); |
| EXPECT_EQ(kChannel, new_port->name()); |
| EXPECT_FALSE(new_port->is_closed_for_testing()); |
| } |
| |
| // Tests sending a one-time message through the messaging service and getting a |
| // response to a callback. Note that this is more thoroughly tested in the |
| // OneTimeMessageHandler tests; this is just to ensure |
| // NativeRendererMessagingService correctly forwards the calls. |
| TEST_F(NativeRendererMessagingServiceTest, SendOneTimeMessageWithCallback) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| const mojom::ChannelType kChannel = mojom::ChannelType::kSendMessage; |
| PortId port_id(script_context()->context_id(), 0, true, |
| mojom::SerializationFormat::kJson); |
| const char kEchoArgs[] = |
| "(function() { this.replyArgs = Array.from(arguments); })"; |
| v8::Local<v8::Function> response_callback = |
| FunctionFromString(context, kEchoArgs); |
| |
| // Send a message and expect a reply to a passed in callback. A new port |
| // should be created, and should remain open until the response is sent. |
| const Message message("\"hi\"", mojom::SerializationFormat::kJson, false); |
| MessageTarget target(MessageTarget::ForExtension(extension()->id())); |
| MockMessagePortHost mock_message_port_host; |
| auto run_loop = std::make_unique<base::RunLoop>(); |
| EXPECT_CALL(*ipc_message_sender(), |
| SendOpenMessageChannel(script_context(), port_id, target, |
| kChannel, "chrome.runtime.sendMessage", |
| testing::_, testing::_)) |
| .WillOnce([&mock_message_port_host]( |
| ScriptContext* script_context, const PortId& port_id, |
| const MessageTarget& target, |
| mojom::ChannelType channel_type, |
| const std::string& channel_name, |
| mojo::PendingAssociatedRemote<mojom::MessagePort> port, |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| port_host) { |
| port.EnableUnassociatedUsage(); |
| port_host.EnableUnassociatedUsage(); |
| mock_message_port_host.BindReceiver(std::move(port_host)); |
| }); |
| EXPECT_CALL(mock_message_port_host, PostMessage(message)) |
| .WillOnce(base::test::RunClosure(run_loop->QuitClosure())); |
| |
| v8::Local<v8::Promise> promise = messaging_service()->SendOneTimeMessage( |
| script_context(), target, kChannel, message, |
| binding::AsyncResponseType::kCallback, response_callback); |
| // Since this is a callback based request, the returned promise should be |
| // empty. |
| EXPECT_TRUE(promise.IsEmpty()); |
| run_loop->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| EXPECT_TRUE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| |
| run_loop = std::make_unique<base::RunLoop>(); |
| // Respond to the message. The response callback should be triggered, and the |
| // port should be closed. |
| EXPECT_CALL(mock_message_port_host, |
| ClosePort( |
| /*close_channel=*/true, |
| /*error_message=*/testing::Eq(std::nullopt))) |
| .WillOnce(base::test::RunClosure(run_loop->QuitClosure())); |
| messaging_service()->DeliverMessage( |
| script_context_set(), port_id, |
| Message("\"reply\"", mojom::SerializationFormat::kJson, false), nullptr); |
| run_loop->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| EXPECT_EQ("[\"reply\"]", GetStringPropertyFromObject(context->Global(), |
| context, "replyArgs")); |
| EXPECT_FALSE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| } |
| |
| // Similar to the above test, tests sending a one-time message through the |
| // messaging service, but this time using a Promise for the response. |
| TEST_F(NativeRendererMessagingServiceTest, SendOneTimeMessageWithPromise) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| const mojom::ChannelType kChannel = mojom::ChannelType::kSendMessage; |
| PortId port_id(script_context()->context_id(), 0, true, |
| mojom::SerializationFormat::kJson); |
| |
| // Send a message and expect a reply fulfilling a promise. A new port should |
| // be created, and should remain open until the response is sent. |
| const Message message("\"hi\"", mojom::SerializationFormat::kJson, false); |
| MessageTarget target(MessageTarget::ForExtension(extension()->id())); |
| MockMessagePortHost mock_message_port_host; |
| auto run_loop = std::make_unique<base::RunLoop>(); |
| EXPECT_CALL(*ipc_message_sender(), |
| SendOpenMessageChannel(script_context(), port_id, target, |
| kChannel, "chrome.runtime.sendMessage", |
| testing::_, testing::_)) |
| .WillRepeatedly( |
| [&mock_message_port_host]( |
| ScriptContext* script_context, const PortId& port_id, |
| const MessageTarget& target, mojom::ChannelType channel_type, |
| const std::string& channel_name, |
| mojo::PendingAssociatedRemote<mojom::MessagePort> port, |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| port_host) { |
| port.EnableUnassociatedUsage(); |
| port_host.EnableUnassociatedUsage(); |
| mock_message_port_host.BindReceiver(std::move(port_host)); |
| }); |
| EXPECT_CALL(mock_message_port_host, PostMessage(message)) |
| .WillOnce(base::test::RunClosure(run_loop->QuitClosure())); |
| v8::Local<v8::Promise> promise = messaging_service()->SendOneTimeMessage( |
| script_context(), target, kChannel, message, |
| binding::AsyncResponseType::kPromise, v8::Local<v8::Function>()); |
| ASSERT_FALSE(promise.IsEmpty()); |
| EXPECT_EQ(v8::Promise::kPending, promise->State()); |
| run_loop->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| EXPECT_TRUE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| |
| run_loop = std::make_unique<base::RunLoop>(); |
| // Respond to the message. The response callback should be triggered, and the |
| // port should be closed. |
| EXPECT_CALL(mock_message_port_host, |
| ClosePort( |
| /*close_channel=*/true, |
| /*error_message=*/testing::Eq(std::nullopt))) |
| .WillOnce(base::test::RunClosure(run_loop->QuitClosure())); |
| messaging_service()->DeliverMessage( |
| script_context_set(), port_id, |
| Message("\"reply\"", mojom::SerializationFormat::kJson, false), nullptr); |
| run_loop->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| EXPECT_EQ(v8::Promise::kFulfilled, promise->State()); |
| EXPECT_EQ("\"reply\"", V8ToString(promise->Result(), context)); |
| EXPECT_FALSE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| } |
| |
| // Tests receiving a one-time message through the messaging service. Note that |
| // this is more thoroughly tested in the OneTimeMessageHandler tests; this is |
| // just to ensure NativeRendererMessagingService correctly forwards the calls. |
| TEST_F(NativeRendererMessagingServiceTest, ReceiveOneTimeMessage) { |
| RuntimeMessageValidationIgnorer message_validation_ignorer; |
| |
| 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" |
| " reply({data: 'hi'});\n" |
| " });\n" |
| "})"; |
| v8::Local<v8::Function> add_listener = |
| FunctionFromString(context, kRegisterListener); |
| RunFunctionOnGlobal(add_listener, context, 0, nullptr); |
| |
| const std::string kChannel = "chrome.runtime.sendMessage"; |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| const PortId port_id(other_context_id, 0, false, |
| mojom::SerializationFormat::kJson); |
| |
| NativeRendererMessagingService::TabConnectionInfo tab_connection_info; |
| NativeRendererMessagingService::ExternalConnectionInfo |
| external_connection_info; |
| tab_connection_info.frame_id = 0; |
| const int tab_id = 10; |
| GURL source_url("http://example.com"); |
| tab_connection_info.tab = base::Value::Dict().Set("tabId", tab_id); |
| external_connection_info.target_id = extension()->id(); |
| external_connection_info.source_endpoint = |
| MessagingEndpoint::ForExtension(extension()->id()); |
| external_connection_info.source_url = source_url; |
| external_connection_info.guest_process_id = |
| content::kInvalidChildProcessUniqueId; |
| external_connection_info.guest_render_frame_routing_id = 0; |
| |
| mojo::PendingAssociatedRemote<mojom::MessagePortHost> port_host_remote; |
| auto port_host_receiver = |
| port_host_remote.InitWithNewEndpointAndPassReceiver(); |
| |
| mojo::PendingAssociatedReceiver<mojom::MessagePort> port_receiver; |
| auto port_remote = port_receiver.InitWithNewEndpointAndPassRemote(); |
| |
| // Open a receiver for the message. |
| bool port_opened = false; |
| MockMessagePortHost mock_message_port_host; |
| messaging_service()->DispatchOnConnect( |
| script_context_set(), port_id, mojom::ChannelType::kSendMessage, kChannel, |
| tab_connection_info, external_connection_info, std::move(port_receiver), |
| std::move(port_host_remote), nullptr, |
| base::BindLambdaForTesting( |
| [&port_opened](bool success) { port_opened = success; })); |
| port_remote.EnableUnassociatedUsage(); |
| port_host_receiver.EnableUnassociatedUsage(); |
| mock_message_port_host.BindReceiver(std::move(port_host_receiver)); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| EXPECT_TRUE(port_opened); |
| EXPECT_TRUE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| |
| base::RunLoop run_loop; |
| // Post the message to the receiver. The receiver should respond, and the |
| // port should close. |
| EXPECT_CALL(mock_message_port_host, |
| PostMessage(Message(R"({"data":"hi"})", |
| mojom::SerializationFormat::kJson, false))); |
| EXPECT_CALL(mock_message_port_host, |
| ClosePort( |
| /*close_channel=*/true, |
| /*error_message=*/testing::Eq(std::nullopt))) |
| .WillOnce(base::test::RunClosure(run_loop.QuitClosure())); |
| messaging_service()->DeliverMessage( |
| script_context_set(), port_id, |
| Message("\"message\"", mojom::SerializationFormat::kJson, false), |
| nullptr); |
| run_loop.Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| EXPECT_FALSE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| } |
| |
| // Test sending a one-time message from an external source (e.g., a different |
| // extension). This shouldn't conflict with messages sent from the same source. |
| TEST_F(NativeRendererMessagingServiceTest, TestExternalOneTimeMessages) { |
| RuntimeMessageValidationIgnorer message_validation_ignorer; |
| |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| constexpr char kListeners[] = |
| R"((function() { |
| chrome.runtime.onMessage.addListener((message) => { |
| this.onMessageReceived = message; |
| return true; // Keep the channel open. |
| }); |
| chrome.runtime.onMessageExternal.addListener((message) => { |
| this.onMessageExternalReceived = message; |
| return true; // Keep the channel open. |
| }); |
| }))"; |
| |
| v8::Local<v8::Function> add_listener = |
| FunctionFromString(context, kListeners); |
| RunFunctionOnGlobal(add_listener, context, 0, nullptr); |
| |
| base::UnguessableToken other_context_id = base::UnguessableToken::Create(); |
| int next_port_id = 0; |
| const PortId on_message_port_id(other_context_id, ++next_port_id, false, |
| mojom::SerializationFormat::kJson); |
| const PortId on_message_external_port_id(other_context_id, ++next_port_id, |
| false, |
| mojom::SerializationFormat::kJson); |
| |
| auto open_port = |
| [this](const PortId& port_id, const ExtensionId& source_id, |
| mojo::PendingAssociatedRemote<mojom::MessagePort>& port_remote, |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost>& |
| port_host_receiver) { |
| NativeRendererMessagingService::TabConnectionInfo tab_connection_info; |
| NativeRendererMessagingService::ExternalConnectionInfo |
| external_connection_info; |
| tab_connection_info.frame_id = 0; |
| const int tab_id = 10; |
| GURL source_url("http://example.com"); |
| tab_connection_info.tab = base::Value::Dict().Set("tabId", tab_id); |
| |
| external_connection_info.target_id = extension()->id(); |
| external_connection_info.source_endpoint = |
| MessagingEndpoint::ForExtension(source_id); |
| external_connection_info.source_url = source_url; |
| external_connection_info.guest_process_id = |
| content::kInvalidChildProcessUniqueId; |
| external_connection_info.guest_render_frame_routing_id = 0; |
| |
| auto port_host_remote = |
| port_host_receiver.InitWithNewEndpointAndPassRemote(); |
| auto port_receiver = port_remote.InitWithNewEndpointAndPassReceiver(); |
| bool port_opened = false; |
| |
| // Open a receiver for the message. |
| messaging_service()->DispatchOnConnect( |
| script_context_set(), port_id, mojom::ChannelType::kSendMessage, |
| messaging_util::kSendMessageChannel, tab_connection_info, |
| external_connection_info, std::move(port_receiver), |
| std::move(port_host_remote), nullptr, |
| base::BindLambdaForTesting( |
| [&port_opened](bool success) { port_opened = success; })); |
| port_remote.EnableUnassociatedUsage(); |
| port_host_receiver.EnableUnassociatedUsage(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| EXPECT_TRUE(port_opened); |
| EXPECT_TRUE( |
| messaging_service()->HasPortForTesting(script_context(), port_id)); |
| }; |
| |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host_receiver; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> port_remote; |
| |
| open_port(on_message_port_id, extension()->id(), port_remote, |
| port_host_receiver); |
| |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| other_port_host_receiver; |
| mojo::PendingAssociatedRemote<mojom::MessagePort> other_port_remote; |
| const ExtensionId other_extension = |
| crx_file::id_util::GenerateId("different"); |
| open_port(on_message_external_port_id, other_extension, other_port_remote, |
| other_port_host_receiver); |
| |
| base::RunLoop run_loop; |
| MockMessagePortHost mock_message_port_host; |
| mock_message_port_host.BindReceiver(std::move(port_host_receiver)); |
| EXPECT_CALL(mock_message_port_host, ResponsePending()) |
| .WillOnce(base::test::RunClosure(run_loop.QuitClosure())); |
| messaging_service()->DeliverMessage( |
| script_context_set(), on_message_port_id, |
| Message("\"onMessage\"", mojom::SerializationFormat::kJson, false), |
| nullptr); |
| EXPECT_EQ("\"onMessage\"", |
| GetStringPropertyFromObject(context->Global(), context, |
| "onMessageReceived")); |
| EXPECT_EQ("undefined", |
| GetStringPropertyFromObject(context->Global(), context, |
| "onMessageExternalReceived")); |
| run_loop.Run(); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_message_port_host); |
| messaging_service()->DeliverMessage( |
| script_context_set(), on_message_external_port_id, |
| Message("\"onMessageExternal\"", mojom::SerializationFormat::kJson, |
| false), |
| nullptr); |
| EXPECT_EQ("\"onMessage\"", |
| GetStringPropertyFromObject(context->Global(), context, |
| "onMessageReceived")); |
| EXPECT_EQ("\"onMessageExternal\"", |
| GetStringPropertyFromObject(context->Global(), context, |
| "onMessageExternalReceived")); |
| } |
| |
| } // namespace extensions |