blob: e186dfad4ca92f33035df585e61bb439e3519596 [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/native_renderer_messaging_service.h"
#include <map>
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/supports_user_data.h"
#include "content/public/common/child_process_host.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/v8_value_converter.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/manifest_handlers/externally_connectable.h"
#include "extensions/renderer/api_activity_logger.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/get_per_context_data.h"
#include "extensions/renderer/get_script_context.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 "extensions/renderer/script_context_set.h"
#include "extensions/renderer/script_context_set_iterable.h"
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "gin/per_context_data.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_scoped_user_gesture.h"
#include "third_party/blink/public/web/web_scoped_window_focus_allowed_indicator.h"
#include "third_party/blink/public/web/web_user_gesture_indicator.h"
#include "v8/include/v8.h"
namespace extensions {
namespace {
struct MessagingPerContextData : public base::SupportsUserData::Data {
static constexpr char kPerContextDataKey[] =
"extension_messaging_per_context_data";
// All the port objects that exist in this context.
std::map<PortId, v8::Global<v8::Object>> ports;
// The next available context-specific port id.
int next_port_id = 0;
};
constexpr char MessagingPerContextData::kPerContextDataKey[];
bool ScriptContextIsValid(ScriptContext* script_context) {
// TODO(devlin): This is in lieu of a similar check in the JS bindings that
// null-checks ScriptContext::GetRenderFrame(). This is good because it
// removes the reliance on RenderFrames (which make testing very difficult),
// but is it fully analogous? It should be close enough, since the browser
// has to deal with frames potentially disappearing before the IPC arrives
// anyway.
return script_context->is_valid();
}
} // namespace
NativeRendererMessagingService::NativeRendererMessagingService(
NativeExtensionBindingsSystem* bindings_system)
: bindings_system_(bindings_system),
one_time_message_handler_(bindings_system) {}
NativeRendererMessagingService::~NativeRendererMessagingService() {}
void NativeRendererMessagingService::ValidateMessagePort(
ScriptContextSetIterable* context_set,
const PortId& port_id,
content::RenderFrame* render_frame) {
// TODO(devlin): In practice, |render_frame| should never be null here (unlike
// in other methods, where it legitimately can), but it can be in testing. It
// would be better to fake it somehow, but unfortunately, there's no good way
// to have a RenderFrame in a unittest. :(
int routing_id =
render_frame ? render_frame->GetRoutingID() : MSG_ROUTING_NONE;
bool has_port = false;
// The base::Unretained() below is safe since ScriptContextSet::ForEach is
// synchronous.
context_set->ForEach(
render_frame,
base::Bind(&NativeRendererMessagingService::ValidateMessagePortInContext,
base::Unretained(this), port_id, &has_port));
// A reply is only sent if the port is missing, because the port is assumed to
// exist unless stated otherwise.
if (!has_port) {
bindings_system_->GetIPCMessageSender()->SendCloseMessagePort(
routing_id, port_id, false);
}
}
void NativeRendererMessagingService::DispatchOnConnect(
ScriptContextSetIterable* context_set,
const PortId& target_port_id,
const std::string& channel_name,
const ExtensionMsg_TabConnectionInfo& source,
const ExtensionMsg_ExternalConnectionInfo& info,
content::RenderFrame* restrict_to_render_frame) {
DCHECK(!target_port_id.is_opener);
int routing_id = restrict_to_render_frame
? restrict_to_render_frame->GetRoutingID()
: MSG_ROUTING_NONE;
bool port_created = false;
context_set->ForEach(
info.target_id, restrict_to_render_frame,
base::Bind(
&NativeRendererMessagingService::DispatchOnConnectToScriptContext,
base::Unretained(this), target_port_id, channel_name, &source, info,
&port_created));
// Note: |restrict_to_render_frame| may have been deleted at this point!
IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
if (port_created) {
ipc_sender->SendOpenMessagePort(routing_id, target_port_id);
} else {
ipc_sender->SendCloseMessagePort(routing_id, target_port_id, false);
}
}
void NativeRendererMessagingService::DeliverMessage(
ScriptContextSetIterable* context_set,
const PortId& target_port_id,
const Message& message,
content::RenderFrame* restrict_to_render_frame) {
context_set->ForEach(
restrict_to_render_frame,
base::Bind(&NativeRendererMessagingService::DeliverMessageToScriptContext,
base::Unretained(this), message, target_port_id));
}
void NativeRendererMessagingService::DispatchOnDisconnect(
ScriptContextSetIterable* context_set,
const PortId& port_id,
const std::string& error_message,
content::RenderFrame* restrict_to_render_frame) {
context_set->ForEach(
restrict_to_render_frame,
base::Bind(
&NativeRendererMessagingService::DispatchOnDisconnectToScriptContext,
base::Unretained(this), port_id, error_message));
}
gin::Handle<GinPort> NativeRendererMessagingService::Connect(
ScriptContext* script_context,
const MessageTarget& target,
const std::string& channel_name,
bool include_tls_channel_id) {
if (!ScriptContextIsValid(script_context))
return gin::Handle<GinPort>();
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
script_context->v8_context(), kCreateIfMissing);
if (!data)
return gin::Handle<GinPort>();
bool is_opener = true;
gin::Handle<GinPort> port = CreatePort(
script_context, channel_name,
PortId(script_context->context_id(), data->next_port_id++, is_opener));
bindings_system_->GetIPCMessageSender()->SendOpenMessageChannel(
script_context, port->port_id(), target, channel_name,
include_tls_channel_id);
return port;
}
void NativeRendererMessagingService::SendOneTimeMessage(
ScriptContext* script_context,
const MessageTarget& target,
const std::string& method_name,
bool include_tls_channel_id,
const Message& message,
v8::Local<v8::Function> response_callback) {
if (!ScriptContextIsValid(script_context))
return;
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
script_context->v8_context(), kCreateIfMissing);
bool is_opener = true;
PortId port_id(script_context->context_id(), data->next_port_id++, is_opener);
one_time_message_handler_.SendMessage(script_context, port_id, target,
method_name, include_tls_channel_id,
message, response_callback);
}
void NativeRendererMessagingService::PostMessageToPort(
v8::Local<v8::Context> context,
const PortId& port_id,
int routing_id,
std::unique_ptr<Message> message) {
ScriptContext* script_context = GetScriptContextFromV8Context(context);
CHECK(script_context);
if (!ScriptContextIsValid(script_context))
return;
bindings_system_->GetIPCMessageSender()->SendPostMessageToPort(port_id,
*message);
}
void NativeRendererMessagingService::ClosePort(v8::Local<v8::Context> context,
const PortId& port_id,
int routing_id) {
ScriptContext* script_context = GetScriptContextFromV8Context(context);
CHECK(script_context);
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
script_context->v8_context(), kDontCreateIfMissing);
if (!data)
return;
size_t erased = data->ports.erase(port_id);
DCHECK_GT(erased, 0u);
if (!ScriptContextIsValid(script_context))
return;
bool close_channel = true;
bindings_system_->GetIPCMessageSender()->SendCloseMessagePort(
routing_id, port_id, close_channel);
}
gin::Handle<GinPort> NativeRendererMessagingService::CreatePortForTesting(
ScriptContext* script_context,
const std::string& channel_name,
const PortId& port_id) {
return CreatePort(script_context, channel_name, port_id);
}
gin::Handle<GinPort> NativeRendererMessagingService::GetPortForTesting(
ScriptContext* script_context,
const PortId& port_id) {
return GetPort(script_context, port_id);
}
bool NativeRendererMessagingService::HasPortForTesting(
ScriptContext* script_context,
const PortId& port_id) {
return ContextHasMessagePort(script_context, port_id);
}
void NativeRendererMessagingService::ValidateMessagePortInContext(
const PortId& port_id,
bool* has_port,
ScriptContext* script_context) {
if (*has_port)
return; // Stop checking if the port was found.
// No need for |=; we know this is false right now from above.
*has_port = ContextHasMessagePort(script_context, port_id);
}
void NativeRendererMessagingService::DispatchOnConnectToScriptContext(
const PortId& target_port_id,
const std::string& channel_name,
const ExtensionMsg_TabConnectionInfo* source,
const ExtensionMsg_ExternalConnectionInfo& info,
bool* port_created,
ScriptContext* script_context) {
// If the channel was opened by this same context, ignore it. This should only
// happen when messages are sent to an entire process (rather than a single
// frame) as an optimization; otherwise the browser process filters this out.
if (script_context->context_id() == target_port_id.context_id)
return;
// First, determine the event we'll use to connect.
std::string target_extension_id = script_context->GetExtensionID();
bool is_external =
(info.source_endpoint.type == MessagingEndpoint::Type::kExtension ||
info.source_endpoint.type == MessagingEndpoint::Type::kTab) &&
info.source_endpoint.extension_id != target_extension_id;
std::string event_name;
if (info.source_endpoint.type == MessagingEndpoint::Type::kNativeApp) {
event_name = messaging_util::kOnConnectNativeEvent;
} else if (channel_name == messaging_util::kSendRequestChannel) {
event_name = is_external ? messaging_util::kOnRequestExternalEvent
: messaging_util::kOnRequestEvent;
} else if (channel_name == messaging_util::kSendMessageChannel) {
event_name = is_external ? messaging_util::kOnMessageExternalEvent
: messaging_util::kOnMessageEvent;
} else {
event_name = is_external ? messaging_util::kOnConnectExternalEvent
: messaging_util::kOnConnectEvent;
}
// If there are no listeners for the given event, then we know the port won't
// be used in this context.
if (!bindings_system_->HasEventListenerInContext(event_name,
script_context)) {
return;
}
*port_created = true;
DispatchOnConnectToListeners(script_context, target_port_id,
target_extension_id, channel_name, source, info,
event_name);
}
void NativeRendererMessagingService::DeliverMessageToScriptContext(
const Message& message,
const PortId& target_port_id,
ScriptContext* script_context) {
if (!ContextHasMessagePort(script_context, target_port_id))
return;
std::unique_ptr<blink::WebScopedUserGesture> web_user_gesture;
std::unique_ptr<blink::WebScopedWindowFocusAllowedIndicator>
allow_window_focus;
if (message.user_gesture) {
web_user_gesture = std::make_unique<blink::WebScopedUserGesture>(
script_context->web_frame());
if (script_context->web_frame()) {
blink::WebDocument document = script_context->web_frame()->GetDocument();
allow_window_focus =
std::make_unique<blink::WebScopedWindowFocusAllowedIndicator>(
&document);
}
}
DispatchOnMessageToListeners(script_context, message, target_port_id);
}
void NativeRendererMessagingService::DispatchOnDisconnectToScriptContext(
const PortId& port_id,
const std::string& error_message,
ScriptContext* script_context) {
if (!ContextHasMessagePort(script_context, port_id))
return;
DispatchOnDisconnectToListeners(script_context, port_id, error_message);
}
bool NativeRendererMessagingService::ContextHasMessagePort(
ScriptContext* script_context,
const PortId& port_id) {
if (one_time_message_handler_.HasPort(script_context, port_id))
return true;
v8::HandleScope handle_scope(script_context->isolate());
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
script_context->v8_context(), kDontCreateIfMissing);
return data && base::ContainsKey(data->ports, port_id);
}
void NativeRendererMessagingService::DispatchOnConnectToListeners(
ScriptContext* script_context,
const PortId& target_port_id,
const ExtensionId& target_extension_id,
const std::string& channel_name,
const ExtensionMsg_TabConnectionInfo* source,
const ExtensionMsg_ExternalConnectionInfo& info,
const std::string& event_name) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> v8_context = script_context->v8_context();
v8::Context::Scope context_scope(v8_context);
gin::DataObjectBuilder sender_builder(isolate);
if (info.source_endpoint.extension_id)
sender_builder.Set("id", *info.source_endpoint.extension_id);
if (info.source_endpoint.native_app_name) {
sender_builder.Set("nativeApplication",
*info.source_endpoint.native_app_name);
}
if (!info.source_url.is_empty())
sender_builder.Set("url", info.source_url.spec());
if (source->frame_id >= 0)
sender_builder.Set("frameId", source->frame_id);
const Extension* extension = script_context->extension();
if (extension) {
if (!source->tab.empty() && !extension->is_platform_app()) {
sender_builder.Set("tab", content::V8ValueConverter::Create()->ToV8Value(
&source->tab, v8_context));
}
ExternallyConnectableInfo* externally_connectable =
ExternallyConnectableInfo::Get(extension);
if (externally_connectable &&
externally_connectable->accepts_tls_channel_id) {
sender_builder.Set("tlsChannelId", std::string());
}
if (info.guest_process_id != content::ChildProcessHost::kInvalidUniqueID) {
CHECK(Manifest::IsComponentLocation(extension->location()))
<< "GuestProcessId can only be exposed to component extensions.";
sender_builder.Set("guestProcessId", info.guest_process_id)
.Set("guestRenderFrameRoutingId", info.guest_render_frame_routing_id);
}
}
v8::Local<v8::Object> sender = sender_builder.Build();
if (channel_name == "chrome.extension.sendRequest" ||
channel_name == "chrome.runtime.sendMessage") {
one_time_message_handler_.AddReceiver(script_context, target_port_id,
sender, event_name);
} else {
gin::Handle<GinPort> port =
CreatePort(script_context, channel_name, target_port_id);
port->SetSender(v8_context, sender);
std::vector<v8::Local<v8::Value>> args = {port.ToV8()};
bindings_system_->api_system()->event_handler()->FireEventInContext(
event_name, v8_context, &args, nullptr, JSRunner::ResultCallback());
}
// Note: Arbitrary JS may have run; the context may now be deleted.
if (binding::IsContextValid(v8_context) &&
APIActivityLogger::IsLoggingEnabled()) {
auto activity_logging_args =
std::make_unique<base::Value>(base::Value::Type::LIST);
auto& list = activity_logging_args->GetList();
list.reserve(2u);
if (info.source_endpoint.extension_id)
list.emplace_back(*info.source_endpoint.extension_id);
else if (info.source_endpoint.native_app_name)
list.emplace_back(*info.source_endpoint.native_app_name);
else
list.emplace_back();
if (!info.source_url.is_empty())
list.emplace_back(info.source_url.spec());
else
list.emplace_back();
APIActivityLogger::LogEvent(
script_context, event_name,
base::ListValue::From(std::move(activity_logging_args)));
}
}
void NativeRendererMessagingService::DispatchOnMessageToListeners(
ScriptContext* script_context,
const Message& message,
const PortId& target_port_id) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(script_context->v8_context());
if (one_time_message_handler_.DeliverMessage(script_context, message,
target_port_id)) {
return;
}
gin::Handle<GinPort> port = GetPort(script_context, target_port_id);
DCHECK(!port.IsEmpty());
port->DispatchOnMessage(script_context->v8_context(), message);
// Note: Arbitrary JS may have run; the context may now be deleted.
}
void NativeRendererMessagingService::DispatchOnDisconnectToListeners(
ScriptContext* script_context,
const PortId& port_id,
const std::string& error_message) {
v8::Isolate* isolate = script_context->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> v8_context = script_context->v8_context();
v8::Context::Scope context_scope(v8_context);
if (one_time_message_handler_.Disconnect(script_context, port_id,
error_message)) {
return;
}
gin::Handle<GinPort> port = GetPort(script_context, port_id);
DCHECK(!port.IsEmpty());
if (!error_message.empty()) {
// TODO(devlin): Subtle: If the JS event to disconnect the port happens
// asynchronously because JS is suspended, this last error won't be
// correctly set for listeners. Given this exceedingly rare, and shouldn't
// behave too strangely, this is somewhat low priority.
bindings_system_->api_system()->request_handler()->last_error()->SetError(
v8_context, error_message);
}
port->DispatchOnDisconnect(v8_context);
// Note: Arbitrary JS may have run; the context may now be deleted.
if (!binding::IsContextValid(v8_context))
return;
if (!error_message.empty()) {
bindings_system_->api_system()->request_handler()->last_error()->ClearError(
v8_context, true);
}
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
v8_context, kDontCreateIfMissing);
DCHECK(data);
data->ports.erase(port_id);
}
gin::Handle<GinPort> NativeRendererMessagingService::CreatePort(
ScriptContext* script_context,
const std::string& channel_name,
const PortId& port_id) {
// Note: no HandleScope because it would invalidate the gin::Handle::wrapper_.
v8::Isolate* isolate = script_context->isolate();
v8::Local<v8::Context> context = script_context->v8_context();
// Note: needed because gin::CreateHandle infers the context from the active
// context on the isolate.
v8::Context::Scope context_scope(context);
// If this port is an opener, then it should have been created in this
// context. Otherwise, it should have been created in another context, because
// we don't support intra-context message passing.
if (port_id.is_opener)
DCHECK_EQ(port_id.context_id, script_context->context_id());
else
DCHECK_NE(port_id.context_id, script_context->context_id());
content::RenderFrame* render_frame = script_context->GetRenderFrame();
int routing_id =
render_frame ? render_frame->GetRoutingID() : MSG_ROUTING_NONE;
MessagingPerContextData* data =
GetPerContextData<MessagingPerContextData>(context, kCreateIfMissing);
DCHECK(data);
DCHECK(!base::ContainsKey(data->ports, port_id));
gin::Handle<GinPort> port_handle = gin::CreateHandle(
isolate,
new GinPort(context, port_id, routing_id, channel_name,
bindings_system_->api_system()->event_handler(), this));
v8::Local<v8::Object> port_object = port_handle.ToV8().As<v8::Object>();
data->ports[port_id].Reset(isolate, port_object);
return port_handle;
}
gin::Handle<GinPort> NativeRendererMessagingService::GetPort(
ScriptContext* script_context,
const PortId& port_id) {
// Note: no HandleScope because it would invalidate the gin::Handle::wrapper_.
v8::Isolate* isolate = script_context->isolate();
v8::Local<v8::Context> context = script_context->v8_context();
MessagingPerContextData* data = GetPerContextData<MessagingPerContextData>(
script_context->v8_context(), kDontCreateIfMissing);
DCHECK(data);
DCHECK(base::ContainsKey(data->ports, port_id));
GinPort* port = nullptr;
gin::Converter<GinPort*>::FromV8(context->GetIsolate(),
data->ports[port_id].Get(isolate), &port);
CHECK(port);
return gin::CreateHandle(isolate, port);
}
} // namespace extensions