| // 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/runtime_hooks_delegate.h" |
| |
| #include <memory> |
| |
| #include "base/strings/stringprintf.h" |
| #include "components/crx_file/id_util.h" |
| #include "content/public/common/child_process_host.h" |
| #include "extensions/common/api/messaging/serialization_format.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/extension_messages.h" |
| #include "extensions/common/value_builder.h" |
| #include "extensions/renderer/bindings/api_binding_test_util.h" |
| #include "extensions/renderer/message_target.h" |
| #include "extensions/renderer/native_extension_bindings_system.h" |
| #include "extensions/renderer/native_extension_bindings_system_test_base.h" |
| #include "extensions/renderer/native_renderer_messaging_service.h" |
| #include "extensions/renderer/script_context.h" |
| #include "extensions/renderer/script_context_set.h" |
| #include "extensions/renderer/send_message_tester.h" |
| |
| namespace extensions { |
| namespace { |
| |
| void CallAPIAndExpectError(v8::Local<v8::Context> context, |
| base::StringPiece method_name, |
| base::StringPiece args) { |
| SCOPED_TRACE(base::StringPrintf("Args: `%s`", args.data())); |
| constexpr char kTemplate[] = "(function() { chrome.runtime.%s(%s); })"; |
| |
| v8::Isolate* isolate = context->GetIsolate(); |
| |
| // Just verify some error was thrown. Expecting the exact error message |
| // tends to rely too much on our argument spec code, which is tested |
| // separately. |
| v8::Local<v8::Function> function = FunctionFromString( |
| context, base::StringPrintf(kTemplate, method_name.data(), args.data())); |
| v8::TryCatch try_catch(isolate); |
| v8::MaybeLocal<v8::Value> result = |
| function->Call(context, v8::Undefined(isolate), 0, nullptr); |
| EXPECT_TRUE(result.IsEmpty()); |
| EXPECT_TRUE(try_catch.HasCaught()); |
| } |
| |
| } // namespace |
| |
| class RuntimeHooksDelegateTest : public NativeExtensionBindingsSystemUnittest { |
| public: |
| RuntimeHooksDelegateTest() {} |
| |
| RuntimeHooksDelegateTest(const RuntimeHooksDelegateTest&) = delete; |
| RuntimeHooksDelegateTest& operator=(const RuntimeHooksDelegateTest&) = delete; |
| |
| ~RuntimeHooksDelegateTest() override {} |
| |
| // NativeExtensionBindingsSystemUnittest: |
| void SetUp() override { |
| NativeExtensionBindingsSystemUnittest::SetUp(); |
| messaging_service_ = |
| std::make_unique<NativeRendererMessagingService>(bindings_system()); |
| |
| bindings_system()->api_system()->GetHooksForAPI("runtime")->SetDelegate( |
| std::make_unique<RuntimeHooksDelegate>(messaging_service_.get())); |
| |
| extension_ = BuildExtension(); |
| 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; |
| messaging_service_.reset(); |
| NativeExtensionBindingsSystemUnittest::TearDown(); |
| } |
| bool UseStrictIPCMessageSender() override { return true; } |
| |
| virtual scoped_refptr<const Extension> BuildExtension() { |
| return ExtensionBuilder("foo").Build(); |
| } |
| |
| NativeRendererMessagingService* messaging_service() { |
| return messaging_service_.get(); |
| } |
| ScriptContext* script_context() { return script_context_; } |
| const Extension* extension() { return extension_.get(); } |
| |
| private: |
| std::unique_ptr<NativeRendererMessagingService> messaging_service_; |
| |
| ScriptContext* script_context_ = nullptr; |
| scoped_refptr<const Extension> extension_; |
| }; |
| |
| TEST_F(RuntimeHooksDelegateTest, RuntimeId) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| { |
| scoped_refptr<const Extension> connectable_extension = |
| ExtensionBuilder("connectable") |
| .SetManifestPath({"externally_connectable", "matches"}, |
| ListBuilder().Append("*://example.com/*").Build()) |
| .Build(); |
| RegisterExtension(connectable_extension); |
| } |
| |
| auto get_id = [](v8::Local<v8::Context> context) { |
| v8::Local<v8::Function> get_id = FunctionFromString( |
| context, "(function() { return chrome.runtime.id; })"); |
| return RunFunction(get_id, context, 0, nullptr); |
| }; |
| |
| { |
| v8::Local<v8::Value> id = get_id(context); |
| EXPECT_EQ(extension()->id(), gin::V8ToString(isolate(), id)); |
| } |
| |
| { |
| // In order for chrome.runtime to be available to web pages, we need to have |
| // an associated connectable extension, so pretend to be example.com. |
| v8::Local<v8::Context> web_context = AddContext(); |
| ScriptContext* script_context = |
| CreateScriptContext(web_context, nullptr, Feature::WEB_PAGE_CONTEXT); |
| script_context->set_url(GURL("http://example.com")); |
| bindings_system()->UpdateBindingsForContext(script_context); |
| v8::Local<v8::Value> id = get_id(web_context); |
| EXPECT_TRUE(id->IsUndefined()); |
| } |
| } |
| |
| TEST_F(RuntimeHooksDelegateTest, GetManifest) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| v8::Local<v8::Function> get_manifest = FunctionFromString( |
| context, "(function() { return chrome.runtime.getManifest(); })"); |
| v8::Local<v8::Value> manifest = |
| RunFunction(get_manifest, context, 0, nullptr); |
| ASSERT_FALSE(manifest.IsEmpty()); |
| ASSERT_TRUE(manifest->IsObject()); |
| EXPECT_EQ(ValueToString(*extension()->manifest()->value()), |
| V8ToString(manifest, context)); |
| } |
| |
| TEST_F(RuntimeHooksDelegateTest, GetURL) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| auto get_url = [this, context](const char* args, const GURL& expected_url) { |
| SCOPED_TRACE(base::StringPrintf("Args: `%s`", args)); |
| constexpr char kGetUrlTemplate[] = |
| "(function() { return chrome.runtime.getURL(%s); })"; |
| v8::Local<v8::Function> get_url = |
| FunctionFromString(context, base::StringPrintf(kGetUrlTemplate, args)); |
| v8::Local<v8::Value> url = RunFunction(get_url, context, 0, nullptr); |
| ASSERT_FALSE(url.IsEmpty()); |
| ASSERT_TRUE(url->IsString()); |
| EXPECT_EQ(expected_url.spec(), gin::V8ToString(isolate(), url)); |
| }; |
| |
| get_url("''", extension()->url()); |
| get_url("'foo'", extension()->GetResourceURL("foo")); |
| get_url("'/foo'", extension()->GetResourceURL("foo")); |
| get_url("'https://www.google.com'", |
| GURL(extension()->url().spec() + "https://www.google.com")); |
| } |
| |
| TEST_F(RuntimeHooksDelegateTest, Connect) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| { |
| // Sanity check: connectNative is unavailable (missing permission). |
| v8::Local<v8::Value> connect_native = |
| V8ValueFromScriptSource(context, "chrome.runtime.connectNative"); |
| ASSERT_FALSE(connect_native.IsEmpty()); |
| EXPECT_TRUE(connect_native->IsUndefined()); |
| } |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| MessageTarget self_target = MessageTarget::ForExtension(extension()->id()); |
| tester.TestConnect("", "", self_target); |
| tester.TestConnect("{name: 'channel'}", "channel", self_target); |
| tester.TestConnect("{includeTlsChannelId: true}", "", self_target); |
| tester.TestConnect("{includeTlsChannelId: true, name: 'channel'}", "channel", |
| self_target); |
| |
| std::string other_id = crx_file::id_util::GenerateId("other"); |
| MessageTarget other_target = MessageTarget::ForExtension(other_id); |
| tester.TestConnect(base::StringPrintf("'%s'", other_id.c_str()), "", |
| other_target); |
| tester.TestConnect( |
| base::StringPrintf("'%s', {name: 'channel'}", other_id.c_str()), |
| "channel", other_target); |
| } |
| |
| // Tests the end-to-end (renderer) flow for a call to runtime.sendMessage |
| // from the call in JS to the expected IPCs. The intricacies of sendMessage |
| // are also tested more in the renderer messaging service unittests. |
| TEST_F(RuntimeHooksDelegateTest, SendMessage) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| { |
| // Sanity check: sendNativeMessage is unavailable (missing permission). |
| v8::Local<v8::Value> send_native_message = |
| V8ValueFromScriptSource(context, "chrome.runtime.sendNativeMessage"); |
| ASSERT_FALSE(send_native_message.IsEmpty()); |
| EXPECT_TRUE(send_native_message->IsUndefined()); |
| } |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| |
| MessageTarget self_target = MessageTarget::ForExtension(extension()->id()); |
| tester.TestSendMessage("''", R"("")", self_target, SendMessageTester::CLOSED); |
| |
| constexpr char kStandardMessage[] = R"({"data":"hello"})"; |
| tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target, |
| SendMessageTester::CLOSED); |
| tester.TestSendMessage("{data: 'hello'}, function() {}", kStandardMessage, |
| self_target, SendMessageTester::OPEN); |
| tester.TestSendMessage("{data: 'hello'}, {includeTlsChannelId: true}", |
| kStandardMessage, self_target, |
| SendMessageTester::CLOSED); |
| tester.TestSendMessage( |
| "{data: 'hello'}, {includeTlsChannelId: true}, function() {}", |
| kStandardMessage, self_target, SendMessageTester::OPEN); |
| |
| std::string other_id_str = crx_file::id_util::GenerateId("other"); |
| const char* other_id = other_id_str.c_str(); // For easy StringPrintf()ing. |
| MessageTarget other_target = MessageTarget::ForExtension(other_id_str); |
| |
| tester.TestSendMessage(base::StringPrintf("'%s', {data: 'hello'}", other_id), |
| kStandardMessage, other_target, |
| SendMessageTester::CLOSED); |
| tester.TestSendMessage( |
| base::StringPrintf("'%s', {data: 'hello'}, function() {}", other_id), |
| kStandardMessage, other_target, SendMessageTester::OPEN); |
| tester.TestSendMessage(base::StringPrintf("'%s', 'string message'", other_id), |
| R"("string message")", other_target, |
| SendMessageTester::CLOSED); |
| |
| // The sender could omit the ID by passing null or undefined explicitly. |
| // Regression tests for https://crbug.com/828664. |
| tester.TestSendMessage("null, {data: 'hello'}, function() {}", |
| kStandardMessage, self_target, |
| SendMessageTester::OPEN); |
| tester.TestSendMessage("null, 'test', function() {}", R"("test")", |
| self_target, SendMessageTester::OPEN); |
| tester.TestSendMessage("null, 'test'", R"("test")", self_target, |
| SendMessageTester::CLOSED); |
| tester.TestSendMessage("undefined, 'test', function() {}", R"("test")", |
| self_target, SendMessageTester::OPEN); |
| |
| // Funny case. The only required argument is `message`, which can be any type. |
| // This means that if an extension provides a <string, object> pair for the |
| // first three arguments, it could apply to either the target id and the |
| // message or to the message and the connect options. |
| // In this case, we *always* pick the arguments as target id and message, |
| // because they were the first options (connectOptions was added later), and |
| // because connectOptions is pretty rarely used. |
| // TODO(devlin): This is the determination JS has always made, but we could be |
| // a bit more intelligent about it. We could examine the string to see if it's |
| // a valid extension id as well as looking at the properties on the object. |
| // But probably not worth it at this time. |
| tester.TestSendMessage( |
| base::StringPrintf("'%s', {includeTlsChannelId: true}", other_id), |
| R"({"includeTlsChannelId":true})", other_target, |
| SendMessageTester::CLOSED); |
| tester.TestSendMessage( |
| base::StringPrintf("'%s', {includeTlsChannelId: true}, function() {}", |
| other_id), |
| R"({"includeTlsChannelId":true})", other_target, SendMessageTester::OPEN); |
| } |
| |
| // Test that some incorrect invocations of sendMessage() throw errors. |
| TEST_F(RuntimeHooksDelegateTest, SendMessageErrors) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| auto send_message = [context](base::StringPiece args) { |
| CallAPIAndExpectError(context, "sendMessage", args); |
| }; |
| |
| send_message("{data: 'hi'}, {unknownProp: true}"); |
| send_message("'some id', 'some message', 'some other string'"); |
| send_message("'some id', 'some message', {}, {}"); |
| } |
| |
| TEST_F(RuntimeHooksDelegateTest, ConnectWithTrickyOptions) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| |
| MessageTarget self_target = MessageTarget::ForExtension(extension()->id()); |
| { |
| // Even though we parse the message options separately, we do a conversion |
| // of the object passed into the API. This means that something subtle like |
| // this, which throws on the second access of a property, shouldn't trip us |
| // up. |
| constexpr char kTrickyConnectOptions[] = |
| R"({ |
| get name() { |
| if (this.checkedOnce) |
| throw new Error('tricked!'); |
| this.checkedOnce = true; |
| return 'foo'; |
| } |
| })"; |
| tester.TestConnect(kTrickyConnectOptions, "foo", self_target); |
| } |
| { |
| // A different form of trickiness: the options object doesn't have the |
| // name key (which is acceptable, since its optional), but |
| // any attempt to access the key on an object without a value for it results |
| // in an error. Our argument parsing code should protect us from this. |
| constexpr const char kMessWithObjectPrototype[] = |
| R"((function() { |
| Object.defineProperty( |
| Object.prototype, 'name', |
| { get() { throw new Error('tricked!'); } }); |
| }))"; |
| v8::Local<v8::Function> mess_with_proto = |
| FunctionFromString(context, kMessWithObjectPrototype); |
| RunFunction(mess_with_proto, context, 0, nullptr); |
| tester.TestConnect("{}", "", self_target); |
| } |
| } |
| |
| class RuntimeHooksDelegateNativeMessagingTest |
| : public RuntimeHooksDelegateTest { |
| public: |
| RuntimeHooksDelegateNativeMessagingTest() {} |
| ~RuntimeHooksDelegateNativeMessagingTest() override {} |
| |
| scoped_refptr<const Extension> BuildExtension() override { |
| return ExtensionBuilder("foo").AddPermission("nativeMessaging").Build(); |
| } |
| }; |
| |
| TEST_F(RuntimeHooksDelegateNativeMessagingTest, ConnectNative) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| int next_context_port_id = 0; |
| auto run_connect_native = [this, context, &next_context_port_id]( |
| const std::string& args, |
| const std::string& expected_app_name) { |
| // connectNative() doesn't name channels. |
| const std::string kEmptyExpectedChannel; |
| |
| SCOPED_TRACE(base::StringPrintf("Args: '%s'", args.c_str())); |
| constexpr char kAddPortTemplate[] = |
| "(function() { return chrome.runtime.connectNative(%s); })"; |
| PortId expected_port_id(script_context()->context_id(), |
| next_context_port_id++, true, |
| SerializationFormat::kJson); |
| MessageTarget expected_target( |
| MessageTarget::ForNativeApp(expected_app_name)); |
| EXPECT_CALL(*ipc_message_sender(), |
| SendOpenMessageChannel(script_context(), expected_port_id, |
| expected_target, kEmptyExpectedChannel)); |
| |
| v8::Local<v8::Function> add_port = FunctionFromString( |
| context, base::StringPrintf(kAddPortTemplate, args.c_str())); |
| v8::Local<v8::Value> port = RunFunction(add_port, context, 0, nullptr); |
| ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender()); |
| ASSERT_FALSE(port.IsEmpty()); |
| ASSERT_TRUE(port->IsObject()); |
| }; |
| |
| run_connect_native("'native_app'", "native_app"); |
| run_connect_native("'some_other_native_app'", "some_other_native_app"); |
| |
| auto connect_native_error = [context](base::StringPiece args) { |
| CallAPIAndExpectError(context, "connectNative", args); |
| }; |
| connect_native_error("'native_app', {name: 'name'}"); |
| } |
| |
| TEST_F(RuntimeHooksDelegateNativeMessagingTest, SendNativeMessage) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| |
| tester.TestSendNativeMessage("'native_app', {hi:'bye'}", R"({"hi":"bye"})", |
| "native_app"); |
| tester.TestSendNativeMessage( |
| "'another_native_app', {alpha: 2}, function() {}", R"({"alpha":2})", |
| "another_native_app"); |
| |
| auto send_native_message_error = [context](base::StringPiece args) { |
| CallAPIAndExpectError(context, "sendNativeMessage", args); |
| }; |
| |
| send_native_message_error("{data: 'hi'}, function() {}"); |
| send_native_message_error( |
| "'native_app', 'some message', {includeTlsChannelId: true}"); |
| } |
| |
| class RuntimeHooksDelegateMV3Test : public RuntimeHooksDelegateTest { |
| public: |
| RuntimeHooksDelegateMV3Test() = default; |
| ~RuntimeHooksDelegateMV3Test() override = default; |
| |
| scoped_refptr<const Extension> BuildExtension() override { |
| return ExtensionBuilder("foo") |
| .SetManifestKey("manifest_version", 3) |
| .Build(); |
| } |
| }; |
| |
| TEST_F(RuntimeHooksDelegateMV3Test, SendMessageUsingPromise) { |
| v8::HandleScope handle_scope(isolate()); |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| |
| // The port remains open here after the call because in MV3 we return a |
| // promise if the callback parameter is omitted, so we can't use the presence/ |
| // lack of the callback to determine if the caller is/isn't going to handle |
| // the response. |
| MessageTarget self_target = MessageTarget::ForExtension(extension()->id()); |
| tester.TestSendMessage("''", R"("")", self_target, SendMessageTester::OPEN); |
| |
| constexpr char kStandardMessage[] = R"({"data":"hello"})"; |
| { |
| // Calling sendMessage with a callback should result in no value returned. |
| v8::Local<v8::Value> result = tester.TestSendMessage( |
| "{data: 'hello'}, function() {}", kStandardMessage, self_target, |
| SendMessageTester::OPEN); |
| EXPECT_TRUE(result->IsUndefined()); |
| } |
| |
| { |
| // Calling sendMessage without the callback should result in a promise |
| // returned. |
| v8::Local<v8::Value> result = |
| tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target, |
| SendMessageTester::OPEN); |
| v8::Local<v8::Promise> promise; |
| ASSERT_TRUE(GetValueAs(result, &promise)); |
| EXPECT_EQ(v8::Promise::kPending, promise->State()); |
| } |
| } |
| |
| class RuntimeHooksDelegateNativeMessagingMV3Test |
| : public RuntimeHooksDelegateTest { |
| public: |
| RuntimeHooksDelegateNativeMessagingMV3Test() = default; |
| ~RuntimeHooksDelegateNativeMessagingMV3Test() override = default; |
| |
| scoped_refptr<const Extension> BuildExtension() override { |
| return ExtensionBuilder("foo") |
| .SetManifestKey("manifest_version", 3) |
| .AddPermission("nativeMessaging") |
| .Build(); |
| } |
| }; |
| |
| TEST_F(RuntimeHooksDelegateNativeMessagingMV3Test, SendNativeMessage) { |
| v8::HandleScope handle_scope(isolate()); |
| v8::Local<v8::Context> context = MainContext(); |
| |
| SendMessageTester tester(ipc_message_sender(), script_context(), 0, |
| "runtime"); |
| |
| { |
| // Calling sendNativeMessage without the callback should result in a promise |
| // returned. |
| v8::Local<v8::Value> result = tester.TestSendNativeMessage( |
| "'native_app', {hi:'bye'}", R"({"hi":"bye"})", "native_app"); |
| v8::Local<v8::Promise> promise; |
| ASSERT_TRUE(GetValueAs(result, &promise)); |
| EXPECT_EQ(v8::Promise::kPending, promise->State()); |
| } |
| |
| { |
| // Calling sendNativeMessage with a callback should result in no value |
| // returned. |
| v8::Local<v8::Value> result = tester.TestSendNativeMessage( |
| "'another_native_app', {alpha: 2}, function() {}", R"({"alpha":2})", |
| "another_native_app"); |
| EXPECT_TRUE(result->IsUndefined()); |
| } |
| |
| auto send_native_message_error = [context](base::StringPiece args) { |
| CallAPIAndExpectError(context, "sendNativeMessage", args); |
| }; |
| |
| // Invoking the API with incorrect parameters should emit errors. |
| send_native_message_error("{data: 'hi'}, function() {}"); |
| send_native_message_error( |
| "'native_app', 'some message', {includeTlsChannelId: true}"); |
| } |
| |
| } // namespace extensions |