blob: 6fadf03fc4c826dc26217d1847c02e735da451bb [file] [log] [blame]
Devlin Cronindbbb47c2017-10-13 00:50:161// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "extensions/renderer/runtime_hooks_delegate.h"
6
7#include "base/containers/span.h"
8#include "base/strings/string_piece.h"
9#include "base/strings/stringprintf.h"
10#include "content/public/child/v8_value_converter.h"
11#include "extensions/common/api/messaging/message.h"
12#include "extensions/common/extension.h"
13#include "extensions/common/manifest.h"
14#include "extensions/renderer/bindings/api_signature.h"
15#include "extensions/renderer/message_target.h"
16#include "extensions/renderer/messaging_util.h"
17#include "extensions/renderer/native_renderer_messaging_service.h"
18#include "extensions/renderer/script_context.h"
19#include "extensions/renderer/script_context_set.h"
20#include "gin/converter.h"
21#include "gin/dictionary.h"
22
23namespace extensions {
24
25namespace {
26using RequestResult = APIBindingHooks::RequestResult;
27
28constexpr char kExtensionIdRequiredErrorTemplate[] =
29 "chrome.runtime.%s() called from a webpage must "
30 "specify an Extension ID (string) for its first argument.";
31
32// Parses the target from |v8_target_id|, or uses the extension associated with
33// the |script_context| as a default. Returns true on success, and false on
34// failure.
35bool GetTarget(ScriptContext* script_context,
36 v8::Local<v8::Value> v8_target_id,
37 std::string* target_out) {
38 DCHECK(!v8_target_id.IsEmpty());
39
40 std::string target_id;
41 if (v8_target_id->IsNull()) {
42 if (!script_context->extension())
43 return false;
44
45 *target_out = script_context->extension()->id();
46 } else {
47 DCHECK(v8_target_id->IsString());
48 *target_out = gin::V8ToString(v8_target_id);
49 }
50
51 return true;
52}
53
54// The result of trying to parse options passed to a messaging API.
55enum ParseOptionsResult {
56 TYPE_ERROR, // Invalid values were passed.
57 THROWN, // An error was thrown while parsing.
58 SUCCESS, // Parsing succeeded.
59};
60
61struct MessageOptions {
62 std::string channel_name;
63 bool include_tls_channel_id = false;
64};
65
66// Parses the parameters sent to sendMessage or connect, returning the result of
67// the attempted parse. If |check_for_channel_name| is true, also checks for a
68// provided channel name (this is only true for connect() calls). Populates the
69// result in |options_out| or |error_out| (depending on the success of the
70// parse).
71ParseOptionsResult ParseMessageOptions(v8::Local<v8::Context> context,
72 v8::Local<v8::Object> v8_options,
73 bool check_for_channel_name,
74 MessageOptions* options_out,
75 std::string* error_out) {
76 DCHECK(!v8_options.IsEmpty());
77 DCHECK(!v8_options->IsNull());
78
79 v8::Isolate* isolate = context->GetIsolate();
80
81 MessageOptions options;
82
83 // Theoretically, our argument matching code already checked the types of
84 // the properties on v8_connect_options. However, since we don't make an
85 // independent copy, it's possible that author script has super sneaky
86 // getters/setters that change the result each time the property is
87 // queried. Make no assumptions.
88 v8::Local<v8::Value> v8_channel_name;
89 v8::Local<v8::Value> v8_include_tls_channel_id;
90 gin::Dictionary options_dict(isolate, v8_options);
91 if (!options_dict.Get("includeTlsChannelId", &v8_include_tls_channel_id) ||
92 (check_for_channel_name && !options_dict.Get("name", &v8_channel_name))) {
93 return THROWN;
94 }
95
96 if (check_for_channel_name && !v8_channel_name->IsUndefined()) {
97 if (!v8_channel_name->IsString()) {
98 *error_out = "connectInfo.name must be a string.";
99 return TYPE_ERROR;
100 }
101 options.channel_name = gin::V8ToString(v8_channel_name);
102 }
103
104 if (!v8_include_tls_channel_id->IsUndefined()) {
105 if (!v8_include_tls_channel_id->IsBoolean()) {
106 *error_out = "connectInfo.includeTlsChannelId must be a boolean.";
107 return TYPE_ERROR;
108 }
109 options.include_tls_channel_id = v8_include_tls_channel_id->BooleanValue();
110 }
111
112 *options_out = std::move(options);
113 return SUCCESS;
114}
115
116// Massages the sendMessage() arguments into the expected schema. These
117// arguments are ambiguous (could match multiple signatures), so we can't just
118// rely on the normal signature parsing. Sets |arguments| to the result if
119// successful; otherwise leaves |arguments| untouched. (If the massage is
120// unsuccessful, our normal argument parsing code should throw a reasonable
121// error.
122void MassageSendMessageArguments(
123 v8::Isolate* isolate,
124 std::vector<v8::Local<v8::Value>>* arguments_out) {
125 base::span<const v8::Local<v8::Value>> arguments = *arguments_out;
126 if (arguments.empty() || arguments.size() > 4u)
127 return;
128
129 v8::Local<v8::Value> target_id = v8::Null(isolate);
130 v8::Local<v8::Value> message = v8::Null(isolate);
131 v8::Local<v8::Value> options = v8::Null(isolate);
132 v8::Local<v8::Value> response_callback = v8::Null(isolate);
133
134 // If the last argument is a function, it is the response callback.
135 // Ignore it for the purposes of further argument parsing.
136 if ((*arguments.rbegin())->IsFunction()) {
137 response_callback = *arguments.rbegin();
138 arguments = arguments.first(arguments.size() - 1);
139 }
140
141 switch (arguments.size()) {
142 case 0:
143 // Required argument (message) is missing.
144 // Early-out and rely on normal signature parsing to report this error.
145 return;
146 case 1:
147 // Argument must be the message.
148 message = arguments[0];
149 break;
150 case 2:
151 // Assume the meaning is (id, message) if id would be a string.
152 // Otherwise the meaning is (message, options).
153 if (arguments[0]->IsString()) {
154 target_id = arguments[0];
155 message = arguments[1];
156 } else {
157 message = arguments[0];
158 options = arguments[1];
159 }
160 break;
161 case 3:
162 // The meaning in this case is unambiguous.
163 target_id = arguments[0];
164 message = arguments[1];
165 options = arguments[2];
166 break;
167 case 4:
168 // Too many arguments. Early-out and rely on normal signature parsing to
169 // report this error.
170 return;
171 default:
172 NOTREACHED();
173 }
174
175 *arguments_out = {target_id, message, options, response_callback};
176}
177
178// Handler for the extensionId property on chrome.runtime.
179void GetExtensionId(v8::Local<v8::Name> property_name,
180 const v8::PropertyCallbackInfo<v8::Value>& info) {
181 v8::Isolate* isolate = info.GetIsolate();
182 v8::HandleScope handle_scope(isolate);
183 v8::Local<v8::Context> context = info.Holder()->CreationContext();
184
185 ScriptContext* script_context =
186 ScriptContextSet::GetContextByV8Context(context);
187 // This could potentially be invoked after the script context is removed
188 // (unlike the handler calls, which should only be invoked for valid
189 // contexts).
190 if (script_context && script_context->extension()) {
191 info.GetReturnValue().Set(
192 gin::StringToSymbol(isolate, script_context->extension()->id()));
193 }
194}
195
196constexpr char kGetManifest[] = "runtime.getManifest";
197constexpr char kGetURL[] = "runtime.getURL";
198constexpr char kConnect[] = "runtime.connect";
Devlin Cronin63aa35e2017-10-21 01:27:34199constexpr char kConnectNative[] = "runtime.connectNative";
Devlin Cronindbbb47c2017-10-13 00:50:16200constexpr char kSendMessage[] = "runtime.sendMessage";
Devlin Cronin63aa35e2017-10-21 01:27:34201constexpr char kSendNativeMessage[] = "runtime.sendNativeMessage";
Devlin Cronindbbb47c2017-10-13 00:50:16202
203constexpr char kSendMessageChannel[] = "chrome.runtime.sendMessage";
204
205} // namespace
206
207RuntimeHooksDelegate::RuntimeHooksDelegate(
208 NativeRendererMessagingService* messaging_service)
209 : messaging_service_(messaging_service) {}
210RuntimeHooksDelegate::~RuntimeHooksDelegate() {}
211
212RequestResult RuntimeHooksDelegate::HandleRequest(
213 const std::string& method_name,
214 const APISignature* signature,
215 v8::Local<v8::Context> context,
216 std::vector<v8::Local<v8::Value>>* arguments,
217 const APITypeReferenceMap& refs) {
218 using Handler = RequestResult (RuntimeHooksDelegate::*)(
219 ScriptContext*, const std::vector<v8::Local<v8::Value>>&);
220 static const struct {
221 Handler handler;
222 base::StringPiece method;
223 } kHandlers[] = {
224 {&RuntimeHooksDelegate::HandleSendMessage, kSendMessage},
225 {&RuntimeHooksDelegate::HandleConnect, kConnect},
226 {&RuntimeHooksDelegate::HandleGetURL, kGetURL},
227 {&RuntimeHooksDelegate::HandleGetManifest, kGetManifest},
Devlin Cronin63aa35e2017-10-21 01:27:34228 {&RuntimeHooksDelegate::HandleConnectNative, kConnectNative},
229 {&RuntimeHooksDelegate::HandleSendNativeMessage, kSendNativeMessage},
Devlin Cronindbbb47c2017-10-13 00:50:16230 };
231
232 ScriptContext* script_context =
233 ScriptContextSet::GetContextByV8Context(context);
234 DCHECK(script_context);
235
236 Handler handler = nullptr;
237 for (const auto& handler_entry : kHandlers) {
238 if (handler_entry.method == method_name) {
239 handler = handler_entry.handler;
240 break;
241 }
242 }
243
244 if (!handler)
245 return RequestResult(RequestResult::NOT_HANDLED);
246
247 if (method_name == kSendMessage)
248 MassageSendMessageArguments(context->GetIsolate(), arguments);
249
250 std::string error;
251 std::vector<v8::Local<v8::Value>> parsed_arguments;
252 if (!signature->ParseArgumentsToV8(context, *arguments, refs,
253 &parsed_arguments, &error)) {
254 RequestResult result(RequestResult::INVALID_INVOCATION);
255 result.error = std::move(error);
256 return result;
257 }
258
259 return (this->*handler)(script_context, parsed_arguments);
260}
261
262void RuntimeHooksDelegate::InitializeTemplate(
263 v8::Isolate* isolate,
264 v8::Local<v8::ObjectTemplate> object_template,
265 const APITypeReferenceMap& type_refs) {
266 object_template->SetAccessor(gin::StringToSymbol(isolate, "id"),
267 &GetExtensionId);
268}
269
270RequestResult RuntimeHooksDelegate::HandleGetManifest(
271 ScriptContext* script_context,
272 const std::vector<v8::Local<v8::Value>>& parsed_arguments) {
273 DCHECK(script_context->extension());
274
275 RequestResult result(RequestResult::HANDLED);
276 result.return_value = content::V8ValueConverter::Create()->ToV8Value(
277 script_context->extension()->manifest()->value(),
278 script_context->v8_context());
279
280 return result;
281}
282
283RequestResult RuntimeHooksDelegate::HandleGetURL(
284 ScriptContext* script_context,
285 const std::vector<v8::Local<v8::Value>>& arguments) {
286 DCHECK_EQ(1u, arguments.size());
287 DCHECK(arguments[0]->IsString());
288 DCHECK(script_context->extension());
289
290 std::string path = gin::V8ToString(arguments[0]);
291
292 RequestResult result(RequestResult::HANDLED);
293 std::string url = base::StringPrintf(
294 "chrome-extension://%s%s%s", script_context->extension()->id().c_str(),
295 !path.empty() && path[0] == '/' ? "" : "/", path.c_str());
296 result.return_value = gin::StringToV8(script_context->isolate(), url);
297
298 return result;
299}
300
301RequestResult RuntimeHooksDelegate::HandleSendMessage(
302 ScriptContext* script_context,
303 const std::vector<v8::Local<v8::Value>>& arguments) {
304 DCHECK_EQ(4u, arguments.size());
305
306 std::string target_id;
307 if (!GetTarget(script_context, arguments[0], &target_id)) {
308 RequestResult result(RequestResult::INVALID_INVOCATION);
309 result.error =
310 base::StringPrintf(kExtensionIdRequiredErrorTemplate, "sendMessage");
311 return result;
312 }
313
314 v8::Local<v8::Context> v8_context = script_context->v8_context();
315 MessageOptions options;
316 if (!arguments[2]->IsNull()) {
317 std::string error;
318 ParseOptionsResult parse_result = ParseMessageOptions(
319 v8_context, arguments[2].As<v8::Object>(), false, &options, &error);
320 switch (parse_result) {
321 case TYPE_ERROR: {
322 RequestResult result(RequestResult::INVALID_INVOCATION);
323 result.error = std::move(error);
324 return result;
325 }
326 case THROWN:
327 return RequestResult(RequestResult::THROWN);
328 case SUCCESS:
329 break;
330 }
331 }
332
333 v8::Local<v8::Value> v8_message = arguments[1];
Devlin Cronindbbb47c2017-10-13 00:50:16334 std::unique_ptr<Message> message =
335 messaging_util::MessageFromV8(v8_context, v8_message);
336 if (!message) {
337 RequestResult result(RequestResult::INVALID_INVOCATION);
338 result.error = "Illegal argument to runtime.sendMessage for 'message'.";
339 return result;
340 }
341
342 v8::Local<v8::Function> response_callback;
343 if (!arguments[3]->IsNull())
344 response_callback = arguments[3].As<v8::Function>();
345
346 messaging_service_->SendOneTimeMessage(
347 script_context, MessageTarget::ForExtension(target_id),
348 kSendMessageChannel, options.include_tls_channel_id, *message,
349 response_callback);
350
351 return RequestResult(RequestResult::HANDLED);
352}
353
Devlin Cronin63aa35e2017-10-21 01:27:34354RequestResult RuntimeHooksDelegate::HandleSendNativeMessage(
355 ScriptContext* script_context,
356 const std::vector<v8::Local<v8::Value>>& arguments) {
357 DCHECK_EQ(3u, arguments.size());
358
359 std::string application_name = gin::V8ToString(arguments[0]);
360
361 v8::Local<v8::Value> v8_message = arguments[1];
362 DCHECK(!v8_message.IsEmpty());
363 std::unique_ptr<Message> message =
364 messaging_util::MessageFromV8(script_context->v8_context(), v8_message);
365 if (!message) {
366 RequestResult result(RequestResult::INVALID_INVOCATION);
367 result.error =
368 "Illegal argument to runtime.sendNativeMessage for 'message'.";
369 return result;
370 }
371
372 v8::Local<v8::Function> response_callback;
373 if (!arguments[2]->IsNull())
374 response_callback = arguments[2].As<v8::Function>();
375
376 messaging_service_->SendOneTimeMessage(
377 script_context, MessageTarget::ForNativeApp(application_name),
378 std::string(), false, *message, response_callback);
379
380 return RequestResult(RequestResult::HANDLED);
381}
382
Devlin Cronindbbb47c2017-10-13 00:50:16383RequestResult RuntimeHooksDelegate::HandleConnect(
384 ScriptContext* script_context,
385 const std::vector<v8::Local<v8::Value>>& arguments) {
386 DCHECK_EQ(2u, arguments.size());
387
388 std::string target_id;
389 if (!GetTarget(script_context, arguments[0], &target_id)) {
390 RequestResult result(RequestResult::INVALID_INVOCATION);
391 result.error =
392 base::StringPrintf(kExtensionIdRequiredErrorTemplate, "connect");
393 return result;
394 }
395
396 MessageOptions options;
397 if (!arguments[1]->IsNull()) {
398 std::string error;
399 ParseOptionsResult parse_result = ParseMessageOptions(
400 script_context->v8_context(), arguments[1].As<v8::Object>(), true,
401 &options, &error);
402 switch (parse_result) {
403 case TYPE_ERROR: {
404 RequestResult result(RequestResult::INVALID_INVOCATION);
405 result.error = std::move(error);
406 return result;
407 }
408 case THROWN:
409 return RequestResult(RequestResult::THROWN);
410 case SUCCESS:
411 break;
412 }
413 }
414
415 gin::Handle<GinPort> port = messaging_service_->Connect(
416 script_context, MessageTarget::ForExtension(target_id),
417 options.channel_name, options.include_tls_channel_id);
418 DCHECK(!port.IsEmpty());
419
420 RequestResult result(RequestResult::HANDLED);
421 result.return_value = port.ToV8();
422 return result;
423}
424
Devlin Cronin63aa35e2017-10-21 01:27:34425RequestResult RuntimeHooksDelegate::HandleConnectNative(
426 ScriptContext* script_context,
427 const std::vector<v8::Local<v8::Value>>& arguments) {
428 DCHECK_EQ(1u, arguments.size());
429 DCHECK(arguments[0]->IsString());
430
431 std::string application_name = gin::V8ToString(arguments[0]);
432 gin::Handle<GinPort> port = messaging_service_->Connect(
433 script_context, MessageTarget::ForNativeApp(application_name),
434 std::string(), false);
435
436 RequestResult result(RequestResult::HANDLED);
437 result.return_value = port.ToV8();
438 return result;
439}
440
Devlin Cronindbbb47c2017-10-13 00:50:16441} // namespace extensions