blob: 998c002159642eb3a54b45669d17652b969fad3e [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/runtime_hooks_delegate.h"
#include "base/containers/span.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "content/public/renderer/v8_value_converter.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest.h"
#include "extensions/renderer/bindings/api_signature.h"
#include "extensions/renderer/message_target.h"
#include "extensions/renderer/messaging_util.h"
#include "extensions/renderer/native_renderer_messaging_service.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/script_context_set.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
namespace extensions {
namespace {
using RequestResult = APIBindingHooks::RequestResult;
constexpr char kExtensionIdRequiredErrorTemplate[] =
"chrome.runtime.%s() called from a webpage must "
"specify an Extension ID (string) for its first argument.";
// Parses the target from |v8_target_id|, or uses the extension associated with
// the |script_context| as a default. Returns true on success, and false on
// failure.
bool GetTarget(ScriptContext* script_context,
v8::Local<v8::Value> v8_target_id,
std::string* target_out) {
DCHECK(!v8_target_id.IsEmpty());
std::string target_id;
if (v8_target_id->IsNull()) {
if (!script_context->extension())
return false;
*target_out = script_context->extension()->id();
} else {
DCHECK(v8_target_id->IsString());
*target_out = gin::V8ToString(v8_target_id);
}
return true;
}
// The result of trying to parse options passed to a messaging API.
enum ParseOptionsResult {
TYPE_ERROR, // Invalid values were passed.
THROWN, // An error was thrown while parsing.
SUCCESS, // Parsing succeeded.
};
struct MessageOptions {
std::string channel_name;
bool include_tls_channel_id = false;
};
// Parses the parameters sent to sendMessage or connect, returning the result of
// the attempted parse. If |check_for_channel_name| is true, also checks for a
// provided channel name (this is only true for connect() calls). Populates the
// result in |options_out| or |error_out| (depending on the success of the
// parse).
ParseOptionsResult ParseMessageOptions(v8::Local<v8::Context> context,
v8::Local<v8::Object> v8_options,
bool check_for_channel_name,
MessageOptions* options_out,
std::string* error_out) {
DCHECK(!v8_options.IsEmpty());
DCHECK(!v8_options->IsNull());
v8::Isolate* isolate = context->GetIsolate();
MessageOptions options;
// Theoretically, our argument matching code already checked the types of
// the properties on v8_connect_options. However, since we don't make an
// independent copy, it's possible that author script has super sneaky
// getters/setters that change the result each time the property is
// queried. Make no assumptions.
v8::Local<v8::Value> v8_channel_name;
v8::Local<v8::Value> v8_include_tls_channel_id;
gin::Dictionary options_dict(isolate, v8_options);
if (!options_dict.Get("includeTlsChannelId", &v8_include_tls_channel_id) ||
(check_for_channel_name && !options_dict.Get("name", &v8_channel_name))) {
return THROWN;
}
if (check_for_channel_name && !v8_channel_name->IsUndefined()) {
if (!v8_channel_name->IsString()) {
*error_out = "connectInfo.name must be a string.";
return TYPE_ERROR;
}
options.channel_name = gin::V8ToString(v8_channel_name);
}
if (!v8_include_tls_channel_id->IsUndefined()) {
if (!v8_include_tls_channel_id->IsBoolean()) {
*error_out = "connectInfo.includeTlsChannelId must be a boolean.";
return TYPE_ERROR;
}
options.include_tls_channel_id = v8_include_tls_channel_id->BooleanValue();
}
*options_out = std::move(options);
return SUCCESS;
}
// Massages the sendMessage() arguments into the expected schema. These
// arguments are ambiguous (could match multiple signatures), so we can't just
// rely on the normal signature parsing. Sets |arguments| to the result if
// successful; otherwise leaves |arguments| untouched. (If the massage is
// unsuccessful, our normal argument parsing code should throw a reasonable
// error.
void MassageSendMessageArguments(
v8::Isolate* isolate,
std::vector<v8::Local<v8::Value>>* arguments_out) {
base::span<const v8::Local<v8::Value>> arguments = *arguments_out;
if (arguments.empty() || arguments.size() > 4u)
return;
v8::Local<v8::Value> target_id = v8::Null(isolate);
v8::Local<v8::Value> message = v8::Null(isolate);
v8::Local<v8::Value> options = v8::Null(isolate);
v8::Local<v8::Value> response_callback = v8::Null(isolate);
// If the last argument is a function, it is the response callback.
// Ignore it for the purposes of further argument parsing.
if ((*arguments.rbegin())->IsFunction()) {
response_callback = *arguments.rbegin();
arguments = arguments.first(arguments.size() - 1);
}
switch (arguments.size()) {
case 0:
// Required argument (message) is missing.
// Early-out and rely on normal signature parsing to report this error.
return;
case 1:
// Argument must be the message.
message = arguments[0];
break;
case 2:
// Assume the meaning is (id, message) if id would be a string.
// Otherwise the meaning is (message, options).
if (arguments[0]->IsString()) {
target_id = arguments[0];
message = arguments[1];
} else {
message = arguments[0];
options = arguments[1];
}
break;
case 3:
// The meaning in this case is unambiguous.
target_id = arguments[0];
message = arguments[1];
options = arguments[2];
break;
case 4:
// Too many arguments. Early-out and rely on normal signature parsing to
// report this error.
return;
default:
NOTREACHED();
}
*arguments_out = {target_id, message, options, response_callback};
}
// Handler for the extensionId property on chrome.runtime.
void GetExtensionId(v8::Local<v8::Name> property_name,
const v8::PropertyCallbackInfo<v8::Value>& info) {
v8::Isolate* isolate = info.GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = info.Holder()->CreationContext();
ScriptContext* script_context =
ScriptContextSet::GetContextByV8Context(context);
// This could potentially be invoked after the script context is removed
// (unlike the handler calls, which should only be invoked for valid
// contexts).
if (script_context && script_context->extension()) {
info.GetReturnValue().Set(
gin::StringToSymbol(isolate, script_context->extension()->id()));
}
}
constexpr char kGetManifest[] = "runtime.getManifest";
constexpr char kGetURL[] = "runtime.getURL";
constexpr char kConnect[] = "runtime.connect";
constexpr char kConnectNative[] = "runtime.connectNative";
constexpr char kSendMessage[] = "runtime.sendMessage";
constexpr char kSendNativeMessage[] = "runtime.sendNativeMessage";
constexpr char kSendMessageChannel[] = "chrome.runtime.sendMessage";
} // namespace
RuntimeHooksDelegate::RuntimeHooksDelegate(
NativeRendererMessagingService* messaging_service)
: messaging_service_(messaging_service) {}
RuntimeHooksDelegate::~RuntimeHooksDelegate() {}
RequestResult RuntimeHooksDelegate::HandleRequest(
const std::string& method_name,
const APISignature* signature,
v8::Local<v8::Context> context,
std::vector<v8::Local<v8::Value>>* arguments,
const APITypeReferenceMap& refs) {
using Handler = RequestResult (RuntimeHooksDelegate::*)(
ScriptContext*, const std::vector<v8::Local<v8::Value>>&);
static const struct {
Handler handler;
base::StringPiece method;
} kHandlers[] = {
{&RuntimeHooksDelegate::HandleSendMessage, kSendMessage},
{&RuntimeHooksDelegate::HandleConnect, kConnect},
{&RuntimeHooksDelegate::HandleGetURL, kGetURL},
{&RuntimeHooksDelegate::HandleGetManifest, kGetManifest},
{&RuntimeHooksDelegate::HandleConnectNative, kConnectNative},
{&RuntimeHooksDelegate::HandleSendNativeMessage, kSendNativeMessage},
};
ScriptContext* script_context =
ScriptContextSet::GetContextByV8Context(context);
DCHECK(script_context);
Handler handler = nullptr;
for (const auto& handler_entry : kHandlers) {
if (handler_entry.method == method_name) {
handler = handler_entry.handler;
break;
}
}
if (!handler)
return RequestResult(RequestResult::NOT_HANDLED);
if (method_name == kSendMessage)
MassageSendMessageArguments(context->GetIsolate(), arguments);
std::string error;
std::vector<v8::Local<v8::Value>> parsed_arguments;
if (!signature->ParseArgumentsToV8(context, *arguments, refs,
&parsed_arguments, &error)) {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error = std::move(error);
return result;
}
return (this->*handler)(script_context, parsed_arguments);
}
void RuntimeHooksDelegate::InitializeTemplate(
v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> object_template,
const APITypeReferenceMap& type_refs) {
object_template->SetAccessor(gin::StringToSymbol(isolate, "id"),
&GetExtensionId);
}
RequestResult RuntimeHooksDelegate::HandleGetManifest(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& parsed_arguments) {
DCHECK(script_context->extension());
RequestResult result(RequestResult::HANDLED);
result.return_value = content::V8ValueConverter::Create()->ToV8Value(
script_context->extension()->manifest()->value(),
script_context->v8_context());
return result;
}
RequestResult RuntimeHooksDelegate::HandleGetURL(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& arguments) {
DCHECK_EQ(1u, arguments.size());
DCHECK(arguments[0]->IsString());
DCHECK(script_context->extension());
std::string path = gin::V8ToString(arguments[0]);
RequestResult result(RequestResult::HANDLED);
std::string url = base::StringPrintf(
"chrome-extension://%s%s%s", script_context->extension()->id().c_str(),
!path.empty() && path[0] == '/' ? "" : "/", path.c_str());
result.return_value = gin::StringToV8(script_context->isolate(), url);
return result;
}
RequestResult RuntimeHooksDelegate::HandleSendMessage(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& arguments) {
DCHECK_EQ(4u, arguments.size());
std::string target_id;
if (!GetTarget(script_context, arguments[0], &target_id)) {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error =
base::StringPrintf(kExtensionIdRequiredErrorTemplate, "sendMessage");
return result;
}
v8::Local<v8::Context> v8_context = script_context->v8_context();
MessageOptions options;
if (!arguments[2]->IsNull()) {
std::string error;
ParseOptionsResult parse_result = ParseMessageOptions(
v8_context, arguments[2].As<v8::Object>(), false, &options, &error);
switch (parse_result) {
case TYPE_ERROR: {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error = std::move(error);
return result;
}
case THROWN:
return RequestResult(RequestResult::THROWN);
case SUCCESS:
break;
}
}
v8::Local<v8::Value> v8_message = arguments[1];
std::unique_ptr<Message> message =
messaging_util::MessageFromV8(v8_context, v8_message);
if (!message) {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error = "Illegal argument to runtime.sendMessage for 'message'.";
return result;
}
v8::Local<v8::Function> response_callback;
if (!arguments[3]->IsNull())
response_callback = arguments[3].As<v8::Function>();
messaging_service_->SendOneTimeMessage(
script_context, MessageTarget::ForExtension(target_id),
kSendMessageChannel, options.include_tls_channel_id, *message,
response_callback);
return RequestResult(RequestResult::HANDLED);
}
RequestResult RuntimeHooksDelegate::HandleSendNativeMessage(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& arguments) {
DCHECK_EQ(3u, arguments.size());
std::string application_name = gin::V8ToString(arguments[0]);
v8::Local<v8::Value> v8_message = arguments[1];
DCHECK(!v8_message.IsEmpty());
std::unique_ptr<Message> message =
messaging_util::MessageFromV8(script_context->v8_context(), v8_message);
if (!message) {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error =
"Illegal argument to runtime.sendNativeMessage for 'message'.";
return result;
}
v8::Local<v8::Function> response_callback;
if (!arguments[2]->IsNull())
response_callback = arguments[2].As<v8::Function>();
messaging_service_->SendOneTimeMessage(
script_context, MessageTarget::ForNativeApp(application_name),
std::string(), false, *message, response_callback);
return RequestResult(RequestResult::HANDLED);
}
RequestResult RuntimeHooksDelegate::HandleConnect(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& arguments) {
DCHECK_EQ(2u, arguments.size());
std::string target_id;
if (!GetTarget(script_context, arguments[0], &target_id)) {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error =
base::StringPrintf(kExtensionIdRequiredErrorTemplate, "connect");
return result;
}
MessageOptions options;
if (!arguments[1]->IsNull()) {
std::string error;
ParseOptionsResult parse_result = ParseMessageOptions(
script_context->v8_context(), arguments[1].As<v8::Object>(), true,
&options, &error);
switch (parse_result) {
case TYPE_ERROR: {
RequestResult result(RequestResult::INVALID_INVOCATION);
result.error = std::move(error);
return result;
}
case THROWN:
return RequestResult(RequestResult::THROWN);
case SUCCESS:
break;
}
}
gin::Handle<GinPort> port = messaging_service_->Connect(
script_context, MessageTarget::ForExtension(target_id),
options.channel_name, options.include_tls_channel_id);
DCHECK(!port.IsEmpty());
RequestResult result(RequestResult::HANDLED);
result.return_value = port.ToV8();
return result;
}
RequestResult RuntimeHooksDelegate::HandleConnectNative(
ScriptContext* script_context,
const std::vector<v8::Local<v8::Value>>& arguments) {
DCHECK_EQ(1u, arguments.size());
DCHECK(arguments[0]->IsString());
std::string application_name = gin::V8ToString(arguments[0]);
gin::Handle<GinPort> port = messaging_service_->Connect(
script_context, MessageTarget::ForNativeApp(application_name),
std::string(), false);
RequestResult result(RequestResult::HANDLED);
result.return_value = port.ToV8();
return result;
}
} // namespace extensions