| // Copyright 2018 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 "ios/web/js_messaging/web_frame_impl.h" |
| |
| #import <Foundation/Foundation.h> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/ios/ios_util.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/values.h" |
| #include "crypto/aead.h" |
| #include "crypto/random.h" |
| #import "ios/web/js_messaging/java_script_content_world.h" |
| #import "ios/web/js_messaging/web_view_js_utils.h" |
| #include "ios/web/public/thread/web_task_traits.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| const char kJavaScriptReplyCommandPrefix[] = "frameMessaging_"; |
| |
| // Creates a JavaScript string for executing the function __gCrWeb.|name| with |
| // |parameters|. |
| NSString* CreateFunctionCallWithParamaters( |
| const std::string& name, |
| const std::vector<base::Value>& parameters) { |
| NSMutableArray* parameter_strings = [[NSMutableArray alloc] init]; |
| for (const auto& value : parameters) { |
| std::string string_value; |
| base::JSONWriter::Write(value, &string_value); |
| [parameter_strings addObject:base::SysUTF8ToNSString(string_value)]; |
| } |
| |
| return [NSString |
| stringWithFormat:@"__gCrWeb.%s(%@)", name.c_str(), |
| [parameter_strings componentsJoinedByString:@","]]; |
| } |
| } |
| |
| namespace web { |
| |
| const double kJavaScriptFunctionCallDefaultTimeout = 100.0; |
| |
| WebFrameImpl::WebFrameImpl(WKFrameInfo* frame_info, |
| const std::string& frame_id, |
| bool is_main_frame, |
| GURL security_origin, |
| web::WebState* web_state) |
| : frame_info_(frame_info), |
| frame_id_(frame_id), |
| is_main_frame_(is_main_frame), |
| security_origin_(security_origin), |
| web_state_(web_state), |
| weak_ptr_factory_(this) { |
| DCHECK(frame_info_); |
| DCHECK(web_state_); |
| web_state->AddObserver(this); |
| |
| subscription_ = web_state->AddScriptCommandCallback( |
| base::BindRepeating(&WebFrameImpl::OnJavaScriptReply, |
| base::Unretained(this), base::Unretained(web_state)), |
| GetScriptCommandPrefix()); |
| } |
| |
| WebFrameImpl::~WebFrameImpl() { |
| CancelPendingRequests(); |
| DetachFromWebState(); |
| } |
| |
| WebFrameInternal* WebFrameImpl::GetWebFrameInternal() { |
| return this; |
| } |
| |
| void WebFrameImpl::SetEncryptionKey( |
| std::unique_ptr<crypto::SymmetricKey> frame_key) { |
| frame_key_ = std::move(frame_key); |
| } |
| |
| void WebFrameImpl::SetNextMessageId(int message_id) { |
| next_message_id_ = message_id; |
| } |
| |
| WebState* WebFrameImpl::GetWebState() { |
| return web_state_; |
| } |
| |
| std::string WebFrameImpl::GetFrameId() const { |
| return frame_id_; |
| } |
| |
| bool WebFrameImpl::IsMainFrame() const { |
| return is_main_frame_; |
| } |
| |
| GURL WebFrameImpl::GetSecurityOrigin() const { |
| return security_origin_; |
| } |
| |
| bool WebFrameImpl::CanCallJavaScriptFunction() const { |
| // JavaScript can always be called on the main frame without encryption |
| // because calling the function directly on the webstate with |
| // |ExecuteJavaScript| is secure. However, iframes require an encryption key |
| // in order to securely pass the function name and parameters to the frame. |
| return is_main_frame_ || frame_key_; |
| } |
| |
| BrowserState* WebFrameImpl::GetBrowserState() { |
| return GetWebState()->GetBrowserState(); |
| } |
| |
| const std::string WebFrameImpl::EncryptPayload( |
| base::DictionaryValue payload, |
| const std::string& additiona_data) { |
| crypto::Aead aead(crypto::Aead::AES_256_GCM); |
| aead.Init(&frame_key_->key()); |
| |
| std::string payload_json; |
| base::JSONWriter::Write(payload, &payload_json); |
| std::string payload_iv; |
| crypto::RandBytes(base::WriteInto(&payload_iv, aead.NonceLength() + 1), |
| aead.NonceLength()); |
| std::string payload_ciphertext; |
| if (!aead.Seal(payload_json, payload_iv, additiona_data, |
| &payload_ciphertext)) { |
| LOG(ERROR) << "Error sealing message payload for WebFrame."; |
| return std::string(); |
| } |
| std::string encoded_payload_iv; |
| base::Base64Encode(payload_iv, &encoded_payload_iv); |
| std::string encoded_payload; |
| base::Base64Encode(payload_ciphertext, &encoded_payload); |
| |
| std::string payload_string; |
| base::DictionaryValue payload_dict; |
| payload_dict.SetKey("payload", base::Value(encoded_payload)); |
| payload_dict.SetKey("iv", base::Value(encoded_payload_iv)); |
| base::JSONWriter::Write(payload_dict, &payload_string); |
| return payload_string; |
| } |
| |
| bool WebFrameImpl::CallJavaScriptFunctionInContentWorld( |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| JavaScriptContentWorld* content_world, |
| bool reply_with_result) { |
| int message_id = next_message_id_; |
| next_message_id_++; |
| |
| #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 |
| if (@available(iOS 14, *)) { |
| if (content_world && content_world->GetWKContentWorld()) { |
| return ExecuteJavaScriptFunction(content_world, name, parameters, |
| message_id, reply_with_result); |
| } |
| } |
| #endif // defined(__IPHONE14_0) |
| |
| if (!CanCallJavaScriptFunction()) { |
| return false; |
| } |
| |
| if (!frame_key_) { |
| return ExecuteJavaScriptFunction(name, parameters, message_id, |
| reply_with_result); |
| } |
| |
| base::DictionaryValue message_payload; |
| message_payload.SetKey("messageId", base::Value(message_id)); |
| message_payload.SetKey("replyWithResult", base::Value(reply_with_result)); |
| const std::string& encrypted_message_json = |
| EncryptPayload(std::move(message_payload), std::string()); |
| |
| base::DictionaryValue function_payload; |
| function_payload.SetKey("functionName", base::Value(name)); |
| base::ListValue parameters_value(parameters); |
| function_payload.SetKey("parameters", std::move(parameters_value)); |
| const std::string& encrypted_function_json = EncryptPayload( |
| std::move(function_payload), base::NumberToString(message_id)); |
| |
| if (encrypted_message_json.empty() || encrypted_function_json.empty()) { |
| // Sealing the payload failed. |
| return false; |
| } |
| |
| std::string script = |
| base::StringPrintf("__gCrWeb.message.routeMessage(%s, %s, '%s')", |
| encrypted_message_json.c_str(), |
| encrypted_function_json.c_str(), frame_id_.c_str()); |
| GetWebState()->ExecuteJavaScript(base::UTF8ToUTF16(script)); |
| |
| return true; |
| } |
| |
| bool WebFrameImpl::CallJavaScriptFunction( |
| const std::string& name, |
| const std::vector<base::Value>& parameters) { |
| return CallJavaScriptFunctionInContentWorld(name, parameters, |
| /*content_world=*/nullptr, |
| /*reply_with_result=*/false); |
| } |
| |
| bool WebFrameImpl::CallJavaScriptFunctionInContentWorld( |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| JavaScriptContentWorld* content_world) { |
| return CallJavaScriptFunctionInContentWorld(name, parameters, content_world, |
| /*reply_with_result=*/false); |
| } |
| |
| bool WebFrameImpl::CallJavaScriptFunction( |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| base::OnceCallback<void(const base::Value*)> callback, |
| base::TimeDelta timeout) { |
| return CallJavaScriptFunctionInContentWorld(name, parameters, |
| /*content_world=*/nullptr, |
| std::move(callback), timeout); |
| } |
| |
| bool WebFrameImpl::CallJavaScriptFunctionInContentWorld( |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| JavaScriptContentWorld* content_world, |
| base::OnceCallback<void(const base::Value*)> callback, |
| base::TimeDelta timeout) { |
| int message_id = next_message_id_; |
| |
| auto timeout_callback = std::make_unique<TimeoutCallback>(base::BindOnce( |
| &WebFrameImpl::CancelRequest, base::Unretained(this), message_id)); |
| auto callbacks = std::make_unique<struct RequestCallbacks>( |
| std::move(callback), std::move(timeout_callback)); |
| pending_requests_[message_id] = std::move(callbacks); |
| |
| base::PostDelayedTask( |
| FROM_HERE, {web::WebThread::UI}, |
| pending_requests_[message_id]->timeout_callback->callback(), timeout); |
| bool called = |
| CallJavaScriptFunctionInContentWorld(name, parameters, content_world, |
| /*reply_with_result=*/true); |
| if (!called) { |
| // Remove callbacks if the call failed. |
| auto request = pending_requests_.find(message_id); |
| if (request != pending_requests_.end()) { |
| pending_requests_.erase(request); |
| } |
| } |
| return called; |
| } |
| |
| bool WebFrameImpl::ExecuteJavaScript(const std::string& script) { |
| DCHECK(base::ios::IsRunningOnIOS14OrLater()); |
| DCHECK(frame_info_); |
| |
| if (!IsMainFrame()) { |
| return false; |
| } |
| |
| NSString* ns_script = base::SysUTF8ToNSString(script); |
| void (^completion_handler)(id, NSError*) = ^void(id value, NSError* error) { |
| if (error) { |
| DLOG(WARNING) << "Script execution of:" |
| << base::SysNSStringToUTF16(ns_script) |
| << "\nfailed with error: " |
| << base::SysNSStringToUTF16( |
| error.userInfo[NSLocalizedDescriptionKey]); |
| } |
| }; |
| |
| if (@available(iOS 14.0, *)) { |
| web::ExecuteJavaScript(frame_info_.webView, WKContentWorld.pageWorld, |
| frame_info_, ns_script, completion_handler); |
| return true; |
| } |
| return false; |
| } |
| |
| bool WebFrameImpl::ExecuteJavaScript( |
| const std::string& script, |
| base::OnceCallback<void(const base::Value*)> callback) { |
| DCHECK(base::ios::IsRunningOnIOS14OrLater()); |
| DCHECK(frame_info_); |
| |
| if (!IsMainFrame()) { |
| return false; |
| } |
| |
| NSString* ns_script = base::SysUTF8ToNSString(script); |
| // Because Objective-C blocks treat scoped-variables |
| // as const, we have to redefine the callback with the |
| // __block keyword to be able to run the callback inside |
| // the completion handler. |
| __block auto internal_callback = std::move(callback); |
| void (^completion_handler)(id, NSError*) = ^void(id value, NSError* error) { |
| if (error) { |
| DLOG(WARNING) << "Script execution of:" |
| << base::SysNSStringToUTF16(ns_script) |
| << "\nfailed with error: " |
| << base::SysNSStringToUTF16( |
| error.userInfo[NSLocalizedDescriptionKey]); |
| } else { |
| std::move(internal_callback).Run(ValueResultFromWKResult(value).get()); |
| } |
| }; |
| |
| if (@available(iOS 14.0, *)) { |
| web::ExecuteJavaScript(frame_info_.webView, WKContentWorld.pageWorld, |
| frame_info_, ns_script, completion_handler); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool WebFrameImpl::ExecuteJavaScriptFunction( |
| JavaScriptContentWorld* content_world, |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| int message_id, |
| bool reply_with_result) { |
| DCHECK(content_world); |
| DCHECK(base::ios::IsRunningOnIOS14OrLater()); |
| DCHECK(frame_info_); |
| |
| NSString* script = CreateFunctionCallWithParamaters(name, parameters); |
| |
| void (^completion_handler)(id, NSError*) = nil; |
| if (reply_with_result) { |
| base::WeakPtr<WebFrameImpl> weak_frame = weak_ptr_factory_.GetWeakPtr(); |
| completion_handler = ^void(id value, NSError* error) { |
| if (error) { |
| DLOG(WARNING) << "Script execution of:" |
| << base::SysNSStringToUTF16(script) |
| << "\nfailed with error: " |
| << base::SysNSStringToUTF16( |
| error.userInfo[NSLocalizedDescriptionKey]); |
| } |
| if (weak_frame) { |
| weak_frame->CompleteRequest(message_id, |
| ValueResultFromWKResult(value).get()); |
| } |
| }; |
| } |
| |
| #if defined(__IPHONE_14_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 |
| if (@available(iOS 14.0, *)) { |
| WKContentWorld* world = content_world->GetWKContentWorld(); |
| DCHECK(world); |
| |
| web::ExecuteJavaScript(frame_info_.webView, world, frame_info_, script, |
| completion_handler); |
| return true; |
| } |
| #endif // defined(__IPHONE14_0) |
| |
| return false; |
| } |
| |
| bool WebFrameImpl::ExecuteJavaScriptFunction( |
| const std::string& name, |
| const std::vector<base::Value>& parameters, |
| int message_id, |
| bool reply_with_result) { |
| if (!IsMainFrame()) { |
| return false; |
| } |
| |
| NSString* script = CreateFunctionCallWithParamaters(name, parameters); |
| if (!reply_with_result) { |
| GetWebState()->ExecuteJavaScript(base::SysNSStringToUTF16(script)); |
| return true; |
| } |
| |
| base::WeakPtr<WebFrameImpl> weak_frame = weak_ptr_factory_.GetWeakPtr(); |
| GetWebState()->ExecuteJavaScript(base::SysNSStringToUTF16(script), |
| base::BindOnce(^(const base::Value* result) { |
| if (weak_frame) { |
| weak_frame->CompleteRequest(message_id, |
| result); |
| } |
| })); |
| return true; |
| } |
| |
| void WebFrameImpl::CompleteRequest(int message_id, const base::Value* result) { |
| auto request = pending_requests_.find(message_id); |
| if (request == pending_requests_.end()) { |
| return; |
| } |
| CompleteRequest(std::move(request->second), result); |
| pending_requests_.erase(request); |
| } |
| |
| void WebFrameImpl::CompleteRequest( |
| std::unique_ptr<RequestCallbacks> request_callbacks, |
| const base::Value* result) { |
| request_callbacks->timeout_callback->Cancel(); |
| std::move(request_callbacks->completion).Run(result); |
| } |
| |
| void WebFrameImpl::CancelRequest(int message_id) { |
| CompleteRequest(message_id, /*result=*/nullptr); |
| } |
| |
| void WebFrameImpl::CancelPendingRequests() { |
| for (auto& it : pending_requests_) { |
| CompleteRequest(std::move(it.second), /*result=*/nullptr); |
| } |
| pending_requests_.clear(); |
| } |
| |
| void WebFrameImpl::OnJavaScriptReply(web::WebState* web_state, |
| const base::Value& command_json, |
| const GURL& page_url, |
| bool interacting, |
| WebFrame* sender_frame) { |
| const std::string* command_string = command_json.FindStringKey("command"); |
| if (!command_string || |
| *command_string != (GetScriptCommandPrefix() + ".reply")) { |
| return; |
| } |
| |
| absl::optional<double> message_id = command_json.FindDoubleKey("messageId"); |
| if (!message_id) { |
| return; |
| } |
| |
| CompleteRequest(static_cast<int>(*message_id), |
| command_json.FindKey("result")); |
| } |
| |
| void WebFrameImpl::DetachFromWebState() { |
| if (web_state_) { |
| web_state_->RemoveObserver(this); |
| web_state_ = nullptr; |
| } |
| } |
| |
| const std::string WebFrameImpl::GetScriptCommandPrefix() { |
| return kJavaScriptReplyCommandPrefix + frame_id_; |
| } |
| |
| void WebFrameImpl::WebStateDestroyed(web::WebState* web_state) { |
| CancelPendingRequests(); |
| DetachFromWebState(); |
| } |
| |
| WebFrameImpl::RequestCallbacks::RequestCallbacks( |
| base::OnceCallback<void(const base::Value*)> completion, |
| std::unique_ptr<TimeoutCallback> timeout) |
| : completion(std::move(completion)), timeout_callback(std::move(timeout)) {} |
| |
| WebFrameImpl::RequestCallbacks::~RequestCallbacks() {} |
| |
| } // namespace web |