Devlin Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 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/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 | |
| 23 | namespace extensions { |
| 24 | |
| 25 | namespace { |
| 26 | using RequestResult = APIBindingHooks::RequestResult; |
| 27 | |
| 28 | constexpr 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. |
| 35 | bool 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. |
| 55 | enum ParseOptionsResult { |
| 56 | TYPE_ERROR, // Invalid values were passed. |
| 57 | THROWN, // An error was thrown while parsing. |
| 58 | SUCCESS, // Parsing succeeded. |
| 59 | }; |
| 60 | |
| 61 | struct 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). |
| 71 | ParseOptionsResult 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. |
| 122 | void 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. |
| 179 | void 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 | |
| 196 | constexpr char kGetManifest[] = "runtime.getManifest"; |
| 197 | constexpr char kGetURL[] = "runtime.getURL"; |
| 198 | constexpr char kConnect[] = "runtime.connect"; |
Devlin Cronin | 63aa35e | 2017-10-21 01:27:34 | [diff] [blame^] | 199 | constexpr char kConnectNative[] = "runtime.connectNative"; |
Devlin Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 200 | constexpr char kSendMessage[] = "runtime.sendMessage"; |
Devlin Cronin | 63aa35e | 2017-10-21 01:27:34 | [diff] [blame^] | 201 | constexpr char kSendNativeMessage[] = "runtime.sendNativeMessage"; |
Devlin Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 202 | |
| 203 | constexpr char kSendMessageChannel[] = "chrome.runtime.sendMessage"; |
| 204 | |
| 205 | } // namespace |
| 206 | |
| 207 | RuntimeHooksDelegate::RuntimeHooksDelegate( |
| 208 | NativeRendererMessagingService* messaging_service) |
| 209 | : messaging_service_(messaging_service) {} |
| 210 | RuntimeHooksDelegate::~RuntimeHooksDelegate() {} |
| 211 | |
| 212 | RequestResult 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 Cronin | 63aa35e | 2017-10-21 01:27:34 | [diff] [blame^] | 228 | {&RuntimeHooksDelegate::HandleConnectNative, kConnectNative}, |
| 229 | {&RuntimeHooksDelegate::HandleSendNativeMessage, kSendNativeMessage}, |
Devlin Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 230 | }; |
| 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 | |
| 262 | void 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 | |
| 270 | RequestResult 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 | |
| 283 | RequestResult 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 | |
| 301 | RequestResult 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 Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 334 | 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 Cronin | 63aa35e | 2017-10-21 01:27:34 | [diff] [blame^] | 354 | RequestResult 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 Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 383 | RequestResult 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 Cronin | 63aa35e | 2017-10-21 01:27:34 | [diff] [blame^] | 425 | RequestResult 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 Cronin | dbbb47c | 2017-10-13 00:50:16 | [diff] [blame] | 441 | } // namespace extensions |