1 // 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/one_time_message_handler.h"
6
7 #include <map>
8
9 #include "base/bind.h"
10 #include "base/callback.h"
11 #include "base/stl_util.h"
12 #include "base/supports_user_data.h"
13 #include "content/public/renderer/render_frame.h"
14 #include "extensions/common/api/messaging/message.h"
15 #include "extensions/common/api/messaging/port_id.h"
16 #include "extensions/renderer/bindings/api_binding_util.h"
17 #include "extensions/renderer/bindings/api_bindings_system.h"
18 #include "extensions/renderer/bindings/api_event_handler.h"
19 #include "extensions/renderer/bindings/api_request_handler.h"
20 #include "extensions/renderer/bindings/get_per_context_data.h"
21 #include "extensions/renderer/gc_callback.h"
22 #include "extensions/renderer/ipc_message_sender.h"
23 #include "extensions/renderer/message_target.h"
24 #include "extensions/renderer/messaging_util.h"
25 #include "extensions/renderer/native_extension_bindings_system.h"
26 #include "extensions/renderer/script_context.h"
27 #include "gin/arguments.h"
28 #include "gin/dictionary.h"
29 #include "gin/handle.h"
30 #include "gin/per_context_data.h"
31 #include "ipc/ipc_message.h"
32
33 namespace extensions {
34
35 namespace {
36
37 // An opener port in the context; i.e., the caller of runtime.sendMessage.
38 struct OneTimeOpener {
39 int request_id = -1;
40 int routing_id = MSG_ROUTING_NONE;
41 };
42
43 // A receiver port in the context; i.e., a listener to runtime.onMessage.
44 struct OneTimeReceiver {
45 int routing_id = MSG_ROUTING_NONE;
46 std::string event_name;
47 v8::Global<v8::Object> sender;
48 };
49
50 using OneTimeMessageCallback =
51 base::OnceCallback<void(gin::Arguments* arguments)>;
52 struct OneTimeMessageContextData : public base::SupportsUserData::Data {
53 static constexpr char kPerContextDataKey[] =
54 "extension_one_time_message_context_data";
55
56 std::map<PortId, OneTimeOpener> openers;
57 std::map<PortId, OneTimeReceiver> receivers;
58 std::vector<std::unique_ptr<OneTimeMessageCallback>> pending_callbacks;
59 };
60
61 constexpr char OneTimeMessageContextData::kPerContextDataKey[];
62
RoutingIdForScriptContext(ScriptContext * script_context)63 int RoutingIdForScriptContext(ScriptContext* script_context) {
64 content::RenderFrame* render_frame = script_context->GetRenderFrame();
65 return render_frame ? render_frame->GetRoutingID() : MSG_ROUTING_NONE;
66 }
67
OneTimeMessageResponseHelper(const v8::FunctionCallbackInfo<v8::Value> & info)68 void OneTimeMessageResponseHelper(
69 const v8::FunctionCallbackInfo<v8::Value>& info) {
70 CHECK(info.Data()->IsExternal());
71
72 gin::Arguments arguments(info);
73 v8::Isolate* isolate = arguments.isolate();
74 v8::HandleScope handle_scope(isolate);
75 v8::Local<v8::Context> context = isolate->GetCurrentContext();
76
77 OneTimeMessageContextData* data =
78 GetPerContextData<OneTimeMessageContextData>(context,
79 kDontCreateIfMissing);
80 if (!data)
81 return;
82
83 v8::Local<v8::External> external = info.Data().As<v8::External>();
84 auto* raw_callback = static_cast<OneTimeMessageCallback*>(external->Value());
85 auto iter = std::find_if(
86 data->pending_callbacks.begin(), data->pending_callbacks.end(),
87 [raw_callback](const std::unique_ptr<OneTimeMessageCallback>& callback) {
88 return callback.get() == raw_callback;
89 });
90 if (iter == data->pending_callbacks.end())
91 return;
92
93 std::unique_ptr<OneTimeMessageCallback> callback = std::move(*iter);
94 data->pending_callbacks.erase(iter);
95 std::move(*callback).Run(&arguments);
96 }
97
98 // Called with the results of dispatching an onMessage event to listeners.
99 // Returns true if any of the listeners responded with `true`, indicating they
100 // will respond to the call asynchronously.
WillListenerReplyAsync(v8::Local<v8::Context> context,v8::MaybeLocal<v8::Value> maybe_results)101 bool WillListenerReplyAsync(v8::Local<v8::Context> context,
102 v8::MaybeLocal<v8::Value> maybe_results) {
103 v8::Local<v8::Value> results;
104 // |maybe_results| can be empty if the context was destroyed before the
105 // listeners were ran (or while they were running).
106 if (!maybe_results.ToLocal(&results))
107 return false;
108
109 if (!results->IsObject())
110 return false;
111
112 // Suppress any script errors, but bail out if they happen (in theory, we
113 // shouldn't have any).
114 v8::Isolate* isolate = context->GetIsolate();
115 v8::TryCatch try_catch(isolate);
116 // We expect results in the form of an object with an array of results as
117 // a `results` property.
118 v8::Local<v8::Value> results_property;
119 if (!results.As<v8::Object>()
120 ->Get(context, gin::StringToSymbol(isolate, "results"))
121 .ToLocal(&results_property) ||
122 !results_property->IsArray()) {
123 return false;
124 }
125
126 // Check if any of the results is `true`.
127 v8::Local<v8::Array> array = results_property.As<v8::Array>();
128 uint32_t length = array->Length();
129 for (uint32_t i = 0; i < length; ++i) {
130 v8::Local<v8::Value> val;
131 if (!array->Get(context, i).ToLocal(&val))
132 return false;
133
134 if (val->IsTrue())
135 return true;
136 }
137
138 return false;
139 }
140
141 } // namespace
142
OneTimeMessageHandler(NativeExtensionBindingsSystem * bindings_system)143 OneTimeMessageHandler::OneTimeMessageHandler(
144 NativeExtensionBindingsSystem* bindings_system)
145 : bindings_system_(bindings_system) {}
~OneTimeMessageHandler()146 OneTimeMessageHandler::~OneTimeMessageHandler() {}
147
HasPort(ScriptContext * script_context,const PortId & port_id)148 bool OneTimeMessageHandler::HasPort(ScriptContext* script_context,
149 const PortId& port_id) {
150 v8::Isolate* isolate = script_context->isolate();
151 v8::HandleScope handle_scope(isolate);
152
153 OneTimeMessageContextData* data =
154 GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
155 kDontCreateIfMissing);
156 if (!data)
157 return false;
158 return port_id.is_opener ? base::Contains(data->openers, port_id)
159 : base::Contains(data->receivers, port_id);
160 }
161
SendMessage(ScriptContext * script_context,const PortId & new_port_id,const MessageTarget & target,const std::string & method_name,const Message & message,v8::Local<v8::Function> response_callback)162 void OneTimeMessageHandler::SendMessage(
163 ScriptContext* script_context,
164 const PortId& new_port_id,
165 const MessageTarget& target,
166 const std::string& method_name,
167 const Message& message,
168 v8::Local<v8::Function> response_callback) {
169 v8::Isolate* isolate = script_context->isolate();
170 v8::HandleScope handle_scope(isolate);
171
172 DCHECK(new_port_id.is_opener);
173 DCHECK_EQ(script_context->context_id(), new_port_id.context_id);
174
175 OneTimeMessageContextData* data =
176 GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
177 kCreateIfMissing);
178 DCHECK(data);
179
180 bool wants_response = !response_callback.IsEmpty();
181 int routing_id = RoutingIdForScriptContext(script_context);
182 if (wants_response) {
183 int request_id =
184 bindings_system_->api_system()->request_handler()->AddPendingRequest(
185 script_context->v8_context(), response_callback);
186 OneTimeOpener& port = data->openers[new_port_id];
187 port.request_id = request_id;
188 port.routing_id = routing_id;
189 }
190
191 IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
192 ipc_sender->SendOpenMessageChannel(script_context, new_port_id, target,
193 method_name);
194 ipc_sender->SendPostMessageToPort(new_port_id, message);
195
196 // If the sender doesn't provide a response callback, we can immediately
197 // close the channel. Note: we only do this for extension messages, not
198 // native apps.
199 // TODO(devlin): This is because of some subtle ordering in the browser side,
200 // where closing the channel after sending the message causes things to be
201 // destroyed in the wrong order. That would be nice to fix.
202 if (!wants_response && target.type != MessageTarget::NATIVE_APP) {
203 bool close_channel = true;
204 ipc_sender->SendCloseMessagePort(routing_id, new_port_id, close_channel);
205 }
206 }
207
AddReceiver(ScriptContext * script_context,const PortId & target_port_id,v8::Local<v8::Object> sender,const std::string & event_name)208 void OneTimeMessageHandler::AddReceiver(ScriptContext* script_context,
209 const PortId& target_port_id,
210 v8::Local<v8::Object> sender,
211 const std::string& event_name) {
212 DCHECK(!target_port_id.is_opener);
213 DCHECK_NE(script_context->context_id(), target_port_id.context_id);
214
215 v8::Isolate* isolate = script_context->isolate();
216 v8::HandleScope handle_scope(isolate);
217 v8::Local<v8::Context> context = script_context->v8_context();
218
219 OneTimeMessageContextData* data =
220 GetPerContextData<OneTimeMessageContextData>(context, kCreateIfMissing);
221 DCHECK(data);
222 DCHECK(!base::Contains(data->receivers, target_port_id));
223 OneTimeReceiver& receiver = data->receivers[target_port_id];
224 receiver.sender.Reset(isolate, sender);
225 receiver.routing_id = RoutingIdForScriptContext(script_context);
226 receiver.event_name = event_name;
227 }
228
DeliverMessage(ScriptContext * script_context,const Message & message,const PortId & target_port_id)229 bool OneTimeMessageHandler::DeliverMessage(ScriptContext* script_context,
230 const Message& message,
231 const PortId& target_port_id) {
232 v8::Isolate* isolate = script_context->isolate();
233 v8::HandleScope handle_scope(isolate);
234
235 return target_port_id.is_opener
236 ? DeliverReplyToOpener(script_context, message, target_port_id)
237 : DeliverMessageToReceiver(script_context, message,
238 target_port_id);
239 }
240
Disconnect(ScriptContext * script_context,const PortId & port_id,const std::string & error_message)241 bool OneTimeMessageHandler::Disconnect(ScriptContext* script_context,
242 const PortId& port_id,
243 const std::string& error_message) {
244 v8::Isolate* isolate = script_context->isolate();
245 v8::HandleScope handle_scope(isolate);
246
247 return port_id.is_opener
248 ? DisconnectOpener(script_context, port_id, error_message)
249 : DisconnectReceiver(script_context, port_id);
250 }
251
DeliverMessageToReceiver(ScriptContext * script_context,const Message & message,const PortId & target_port_id)252 bool OneTimeMessageHandler::DeliverMessageToReceiver(
253 ScriptContext* script_context,
254 const Message& message,
255 const PortId& target_port_id) {
256 DCHECK(!target_port_id.is_opener);
257
258 v8::Isolate* isolate = script_context->isolate();
259 v8::Local<v8::Context> context = script_context->v8_context();
260
261 bool handled = false;
262
263 OneTimeMessageContextData* data =
264 GetPerContextData<OneTimeMessageContextData>(context,
265 kDontCreateIfMissing);
266 if (!data)
267 return handled;
268
269 auto iter = data->receivers.find(target_port_id);
270 if (iter == data->receivers.end())
271 return handled;
272
273 handled = true;
274 OneTimeReceiver& port = iter->second;
275
276 // This port is a receiver, so we invoke the onMessage event and provide a
277 // callback through which the port can respond. The port stays open until
278 // we receive a response.
279 // TODO(devlin): With chrome.runtime.sendMessage, we actually require that a
280 // listener return `true` if they intend to respond asynchronously; otherwise
281 // we close the port.
282 auto callback = std::make_unique<OneTimeMessageCallback>(
283 base::Bind(&OneTimeMessageHandler::OnOneTimeMessageResponse,
284 weak_factory_.GetWeakPtr(), target_port_id));
285 v8::Local<v8::External> external = v8::External::New(isolate, callback.get());
286 v8::Local<v8::Function> response_function;
287
288 if (!v8::Function::New(context, &OneTimeMessageResponseHelper, external)
289 .ToLocal(&response_function)) {
290 NOTREACHED();
291 return handled;
292 }
293
294 // We shouldn't need to monitor context invalidation here. We store the ports
295 // for the context in PerContextData (cleaned up on context destruction), and
296 // the browser watches for frame navigation or destruction, and cleans up
297 // orphaned channels.
298 base::Closure on_context_invalidated;
299
300 new GCCallback(
301 script_context, response_function,
302 base::Bind(&OneTimeMessageHandler::OnResponseCallbackCollected,
303 weak_factory_.GetWeakPtr(), script_context, target_port_id),
304 base::Closure());
305
306 v8::HandleScope handle_scope(isolate);
307 v8::Local<v8::Value> v8_message =
308 messaging_util::MessageToV8(context, message);
309 v8::Local<v8::Object> v8_sender = port.sender.Get(isolate);
310 std::vector<v8::Local<v8::Value>> args = {v8_message, v8_sender,
311 response_function};
312
313 JSRunner::ResultCallback dispatch_callback;
314 // For runtime.onMessage, we require that the listener return `true` if they
315 // intend to respond asynchronously. Check the results of the listeners.
316 if (port.event_name == messaging_util::kOnMessageEvent) {
317 dispatch_callback =
318 base::BindOnce(&OneTimeMessageHandler::OnEventFired,
319 weak_factory_.GetWeakPtr(), target_port_id);
320 }
321
322 data->pending_callbacks.push_back(std::move(callback));
323 bindings_system_->api_system()->event_handler()->FireEventInContext(
324 port.event_name, context, &args, nullptr, std::move(dispatch_callback));
325
326 // Note: The context could be invalidated at this point!
327
328 return handled;
329 }
330
DeliverReplyToOpener(ScriptContext * script_context,const Message & message,const PortId & target_port_id)331 bool OneTimeMessageHandler::DeliverReplyToOpener(ScriptContext* script_context,
332 const Message& message,
333 const PortId& target_port_id) {
334 DCHECK(target_port_id.is_opener);
335
336 v8::Local<v8::Context> v8_context = script_context->v8_context();
337 bool handled = false;
338
339 OneTimeMessageContextData* data =
340 GetPerContextData<OneTimeMessageContextData>(v8_context,
341 kDontCreateIfMissing);
342 if (!data)
343 return handled;
344
345 auto iter = data->openers.find(target_port_id);
346 if (iter == data->openers.end())
347 return handled;
348
349 handled = true;
350
351 // Note: make a copy of port, since we're about to free it.
352 const OneTimeOpener port = iter->second;
353 DCHECK_NE(-1, port.request_id);
354
355 // We erase the opener now, since delivering the reply can cause JS to run,
356 // which could either invalidate the context or modify the |openers|
357 // collection (e.g., by sending another message).
358 data->openers.erase(iter);
359
360 // This port was the opener, so the message is the response from the
361 // receiver. Invoke the callback and close the message port.
362 v8::Local<v8::Value> v8_message =
363 messaging_util::MessageToV8(v8_context, message);
364 std::vector<v8::Local<v8::Value>> args = {v8_message};
365 bindings_system_->api_system()->request_handler()->CompleteRequest(
366 port.request_id, args, std::string());
367
368 bool close_channel = true;
369 bindings_system_->GetIPCMessageSender()->SendCloseMessagePort(
370 port.routing_id, target_port_id, close_channel);
371
372 // Note: The context could be invalidated at this point!
373
374 return handled;
375 }
376
DisconnectReceiver(ScriptContext * script_context,const PortId & port_id)377 bool OneTimeMessageHandler::DisconnectReceiver(ScriptContext* script_context,
378 const PortId& port_id) {
379 v8::Local<v8::Context> context = script_context->v8_context();
380 bool handled = false;
381
382 OneTimeMessageContextData* data =
383 GetPerContextData<OneTimeMessageContextData>(context,
384 kDontCreateIfMissing);
385 if (!data)
386 return handled;
387
388 auto iter = data->receivers.find(port_id);
389 if (iter == data->receivers.end())
390 return handled;
391
392 handled = true;
393 data->receivers.erase(iter);
394 return handled;
395 }
396
DisconnectOpener(ScriptContext * script_context,const PortId & port_id,const std::string & error_message)397 bool OneTimeMessageHandler::DisconnectOpener(ScriptContext* script_context,
398 const PortId& port_id,
399 const std::string& error_message) {
400 bool handled = false;
401
402 v8::Local<v8::Context> v8_context = script_context->v8_context();
403 OneTimeMessageContextData* data =
404 GetPerContextData<OneTimeMessageContextData>(v8_context,
405 kDontCreateIfMissing);
406 if (!data)
407 return handled;
408
409 auto iter = data->openers.find(port_id);
410 if (iter == data->openers.end())
411 return handled;
412
413 handled = true;
414
415 // Note: make a copy of port, since we're about to free it.
416 const OneTimeOpener port = iter->second;
417 DCHECK_NE(-1, port.request_id);
418
419 // We erase the opener now, since delivering the reply can cause JS to run,
420 // which could either invalidate the context or modify the |openers|
421 // collection (e.g., by sending another message).
422 data->openers.erase(iter);
423
424 bindings_system_->api_system()->request_handler()->CompleteRequest(
425 port.request_id, std::vector<v8::Local<v8::Value>>(),
426 // If the browser doesn't supply an error message, we supply a generic
427 // one.
428 error_message.empty()
429 ? "The message port closed before a response was received."
430 : error_message);
431
432 // Note: The context could be invalidated at this point!
433
434 return handled;
435 }
436
OnOneTimeMessageResponse(const PortId & port_id,gin::Arguments * arguments)437 void OneTimeMessageHandler::OnOneTimeMessageResponse(
438 const PortId& port_id,
439 gin::Arguments* arguments) {
440 v8::Isolate* isolate = arguments->isolate();
441 v8::Local<v8::Context> context = isolate->GetCurrentContext();
442
443 // The listener may try replying after the context or the channel has been
444 // closed. Fail gracefully.
445 // TODO(devlin): At least in the case of the channel being closed (e.g.
446 // because the listener did not return `true`), it might be good to surface an
447 // error.
448 OneTimeMessageContextData* data =
449 GetPerContextData<OneTimeMessageContextData>(context,
450 kDontCreateIfMissing);
451 if (!data)
452 return;
453
454 auto iter = data->receivers.find(port_id);
455 if (iter == data->receivers.end())
456 return;
457
458 int routing_id = iter->second.routing_id;
459 data->receivers.erase(iter);
460
461 v8::Local<v8::Value> value;
462 // We allow omitting the message argument (e.g., sendMessage()). Default the
463 // value to undefined.
464 if (arguments->Length() > 0)
465 CHECK(arguments->GetNext(&value));
466 else
467 value = v8::Undefined(isolate);
468
469 std::string error;
470 std::unique_ptr<Message> message =
471 messaging_util::MessageFromV8(context, value, &error);
472 if (!message) {
473 arguments->ThrowTypeError(error);
474 return;
475 }
476 IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
477 ipc_sender->SendPostMessageToPort(port_id, *message);
478 bool close_channel = true;
479 ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
480 }
481
OnResponseCallbackCollected(ScriptContext * script_context,const PortId & port_id)482 void OneTimeMessageHandler::OnResponseCallbackCollected(
483 ScriptContext* script_context,
484 const PortId& port_id) {
485 // Note: we know |script_context| is still valid because the GC callback won't
486 // be called after context invalidation.
487 v8::HandleScope handle_scope(script_context->isolate());
488 OneTimeMessageContextData* data =
489 GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(),
490 kDontCreateIfMissing);
491 // ScriptContext invalidation and PerContextData cleanup happen "around" the
492 // same time, but there aren't strict guarantees about ordering. It's possible
493 // the data was collected.
494 if (!data)
495 return;
496
497 auto iter = data->receivers.find(port_id);
498 // The channel may already be closed (if the receiver replied before the reply
499 // callback was collected).
500 if (iter == data->receivers.end())
501 return;
502
503 int routing_id = iter->second.routing_id;
504 data->receivers.erase(iter);
505
506 // Close the message port. There's no way to send a reply anymore. Don't
507 // close the channel because another listener may reply.
508 IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
509 bool close_channel = false;
510 ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
511 }
512
OnEventFired(const PortId & port_id,v8::Local<v8::Context> context,v8::MaybeLocal<v8::Value> result)513 void OneTimeMessageHandler::OnEventFired(const PortId& port_id,
514 v8::Local<v8::Context> context,
515 v8::MaybeLocal<v8::Value> result) {
516 // The context could be tearing down by the time the event is fully
517 // dispatched.
518 OneTimeMessageContextData* data =
519 GetPerContextData<OneTimeMessageContextData>(context,
520 kDontCreateIfMissing);
521 if (!data)
522 return;
523
524 if (WillListenerReplyAsync(context, result))
525 return; // The listener will reply later; leave the channel open.
526
527 auto iter = data->receivers.find(port_id);
528 // The channel may already be closed (if the listener replied).
529 if (iter == data->receivers.end())
530 return;
531
532 int routing_id = iter->second.routing_id;
533 data->receivers.erase(iter);
534
535 // The listener did not reply and did not return `true` from any of its
536 // listeners. Close the message port. Don't close the channel because another
537 // listener (in a separate context) may reply.
538 IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender();
539 bool close_channel = false;
540 ipc_sender->SendCloseMessagePort(routing_id, port_id, close_channel);
541 }
542
543 } // namespace extensions
544