diff options
Diffstat (limited to 'chromium/components/js_injection')
30 files changed, 2376 insertions, 0 deletions
diff --git a/chromium/components/js_injection/DEPS b/chromium/components/js_injection/DEPS new file mode 100644 index 00000000000..74dd110e2d9 --- /dev/null +++ b/chromium/components/js_injection/DEPS @@ -0,0 +1,6 @@ +include_rules = [ + "+content/public", + "+mojo/public", + "+net", + "+third_party/blink/public", +] diff --git a/chromium/components/js_injection/OWNERS b/chromium/components/js_injection/OWNERS new file mode 100644 index 00000000000..e13cff503f9 --- /dev/null +++ b/chromium/components/js_injection/OWNERS @@ -0,0 +1,5 @@ +ctzsm@chromium.org +sky@chromium.org + +# TEAM: android-webview-dev@chromium.org +# COMPONENT: Mobile>WebView diff --git a/chromium/components/js_injection/README.md b/chromium/components/js_injection/README.md new file mode 100644 index 00000000000..1c051b8d2ba --- /dev/null +++ b/chromium/components/js_injection/README.md @@ -0,0 +1,3 @@ +This directory contains code used by WebView and WebLayer to inject +javascript from the browser to the renderer, as well as a simple message +port style API. diff --git a/chromium/components/js_injection/browser/BUILD.gn b/chromium/components/js_injection/browser/BUILD.gn new file mode 100644 index 00000000000..f2b8216f66a --- /dev/null +++ b/chromium/components/js_injection/browser/BUILD.gn @@ -0,0 +1,27 @@ +# Copyright 2020 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. + +source_set("browser") { + sources = [ + "js_communication_host.cc", + "js_communication_host.h", + "js_to_browser_messaging.cc", + "js_to_browser_messaging.h", + "web_message.cc", + "web_message.h", + "web_message_host.h", + "web_message_host_factory.h", + "web_message_reply_proxy.h", + ] + + deps = [ + "//base", + "//components/js_injection/common", + "//components/js_injection/common:common_mojom", + "//content/public/browser", + "//mojo/public/cpp/bindings", + "//third_party/blink/public/common", + "//url", + ] +} diff --git a/chromium/components/js_injection/browser/js_communication_host.cc b/chromium/components/js_injection/browser/js_communication_host.cc new file mode 100644 index 00000000000..f4115b3aa86 --- /dev/null +++ b/chromium/components/js_injection/browser/js_communication_host.cc @@ -0,0 +1,227 @@ +// Copyright 2019 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 "components/js_injection/browser/js_communication_host.h" + +#include "base/bind.h" +#include "base/strings/utf_string_conversions.h" +#include "components/js_injection/browser/js_to_browser_messaging.h" +#include "components/js_injection/browser/web_message_host.h" +#include "components/js_injection/browser/web_message_host_factory.h" +#include "components/js_injection/common/origin_matcher.h" +#include "components/js_injection/common/origin_matcher_mojom_traits.h" +#include "content/public/browser/web_contents.h" +#include "mojo/public/cpp/bindings/pending_associated_remote.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" + +namespace js_injection { +namespace { + +std::string ConvertToNativeAllowedOriginRulesWithSanityCheck( + const std::vector<std::string>& allowed_origin_rules_strings, + OriginMatcher& allowed_origin_rules) { + for (auto& rule : allowed_origin_rules_strings) { + if (!allowed_origin_rules.AddRuleFromString(rule)) + return "allowedOriginRules " + rule + " is invalid"; + } + return std::string(); +} + +} // namespace + +struct JsObject { + JsObject(const base::string16& name, + OriginMatcher allowed_origin_rules, + std::unique_ptr<WebMessageHostFactory> factory) + : name(std::move(name)), + allowed_origin_rules(std::move(allowed_origin_rules)), + factory(std::move(factory)) {} + JsObject(JsObject&& other) = delete; + JsObject& operator=(JsObject&& other) = delete; + ~JsObject() = default; + + base::string16 name; + OriginMatcher allowed_origin_rules; + std::unique_ptr<WebMessageHostFactory> factory; +}; + +struct DocumentStartJavaScript { + DocumentStartJavaScript(base::string16 script, + OriginMatcher allowed_origin_rules, + int32_t script_id) + : script_(std::move(script)), + allowed_origin_rules_(allowed_origin_rules), + script_id_(script_id) {} + + DocumentStartJavaScript(DocumentStartJavaScript&) = delete; + DocumentStartJavaScript& operator=(DocumentStartJavaScript&) = delete; + DocumentStartJavaScript(DocumentStartJavaScript&&) = default; + DocumentStartJavaScript& operator=(DocumentStartJavaScript&&) = default; + + base::string16 script_; + OriginMatcher allowed_origin_rules_; + int32_t script_id_; +}; + +JsCommunicationHost::AddScriptResult::AddScriptResult() = default; +JsCommunicationHost::AddScriptResult::AddScriptResult( + const JsCommunicationHost::AddScriptResult&) = default; +JsCommunicationHost::AddScriptResult& +JsCommunicationHost::AddScriptResult::operator=( + const JsCommunicationHost::AddScriptResult&) = default; +JsCommunicationHost::AddScriptResult::~AddScriptResult() = default; + +JsCommunicationHost::JsCommunicationHost(content::WebContents* web_contents) + : content::WebContentsObserver(web_contents) {} + +JsCommunicationHost::~JsCommunicationHost() = default; + +JsCommunicationHost::AddScriptResult +JsCommunicationHost::AddDocumentStartJavaScript( + const base::string16& script, + const std::vector<std::string>& allowed_origin_rules) { + OriginMatcher origin_matcher; + std::string error_message = ConvertToNativeAllowedOriginRulesWithSanityCheck( + allowed_origin_rules, origin_matcher); + AddScriptResult result; + if (!error_message.empty()) { + result.error_message = std::move(error_message); + return result; + } + + scripts_.emplace_back(script, origin_matcher, next_script_id_++); + + web_contents()->ForEachFrame(base::BindRepeating( + &JsCommunicationHost::NotifyFrameForAddDocumentStartJavaScript, + base::Unretained(this), &*scripts_.rbegin())); + result.script_id = scripts_.rbegin()->script_id_; + return result; +} + +bool JsCommunicationHost::RemoveDocumentStartJavaScript(int script_id) { + for (auto it = scripts_.begin(); it != scripts_.end(); ++it) { + if (it->script_id_ == script_id) { + scripts_.erase(it); + web_contents()->ForEachFrame(base::BindRepeating( + &JsCommunicationHost::NotifyFrameForRemoveDocumentStartJavaScript, + base::Unretained(this), script_id)); + return true; + } + } + return false; +} + +base::string16 JsCommunicationHost::AddWebMessageHostFactory( + std::unique_ptr<WebMessageHostFactory> factory, + const base::string16& js_object_name, + const std::vector<std::string>& allowed_origin_rules) { + OriginMatcher origin_matcher; + std::string error_message = ConvertToNativeAllowedOriginRulesWithSanityCheck( + allowed_origin_rules, origin_matcher); + if (!error_message.empty()) + return base::UTF8ToUTF16(error_message); + + for (const auto& js_object : js_objects_) { + if (js_object->name == js_object_name) { + return base::ASCIIToUTF16("jsObjectName ") + js_object->name + + base::ASCIIToUTF16(" was already added."); + } + } + + js_objects_.push_back(std::make_unique<JsObject>( + js_object_name, origin_matcher, std::move(factory))); + + web_contents()->ForEachFrame(base::BindRepeating( + &JsCommunicationHost::NotifyFrameForWebMessageListener, + base::Unretained(this))); + return base::string16(); +} + +void JsCommunicationHost::RemoveWebMessageHostFactory( + const base::string16& js_object_name) { + for (auto iterator = js_objects_.begin(); iterator != js_objects_.end(); + ++iterator) { + if ((*iterator)->name == js_object_name) { + js_objects_.erase(iterator); + web_contents()->ForEachFrame(base::BindRepeating( + &JsCommunicationHost::NotifyFrameForWebMessageListener, + base::Unretained(this))); + break; + } + } +} + +std::vector<JsCommunicationHost::RegisteredFactory> +JsCommunicationHost::GetWebMessageHostFactories() { + const size_t num_objects = js_objects_.size(); + std::vector<RegisteredFactory> factories(num_objects); + for (size_t i = 0; i < num_objects; ++i) { + factories[i].js_name = js_objects_[i]->name; + factories[i].allowed_origin_rules = js_objects_[i]->allowed_origin_rules; + factories[i].factory = js_objects_[i]->factory.get(); + } + return factories; +} + +void JsCommunicationHost::RenderFrameCreated( + content::RenderFrameHost* render_frame_host) { + NotifyFrameForWebMessageListener(render_frame_host); + NotifyFrameForAllDocumentStartJavaScripts(render_frame_host); +} + +void JsCommunicationHost::RenderFrameDeleted( + content::RenderFrameHost* render_frame_host) { + js_to_browser_messagings_.erase(render_frame_host); +} + +void JsCommunicationHost::NotifyFrameForAllDocumentStartJavaScripts( + content::RenderFrameHost* render_frame_host) { + for (const auto& script : scripts_) { + NotifyFrameForAddDocumentStartJavaScript(&script, render_frame_host); + } +} + +void JsCommunicationHost::NotifyFrameForWebMessageListener( + content::RenderFrameHost* render_frame_host) { + mojo::AssociatedRemote<mojom::JsCommunication> configurator_remote; + render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface( + &configurator_remote); + std::vector<mojom::JsObjectPtr> js_objects; + js_objects.reserve(js_objects_.size()); + for (const auto& js_object : js_objects_) { + mojo::PendingAssociatedRemote<mojom::JsToBrowserMessaging> pending_remote; + js_to_browser_messagings_[render_frame_host].emplace_back( + std::make_unique<JsToBrowserMessaging>( + render_frame_host, + pending_remote.InitWithNewEndpointAndPassReceiver(), + js_object->factory.get(), js_object->allowed_origin_rules)); + js_objects.push_back(mojom::JsObject::New(js_object->name, + std::move(pending_remote), + js_object->allowed_origin_rules)); + } + configurator_remote->SetJsObjects(std::move(js_objects)); +} + +void JsCommunicationHost::NotifyFrameForAddDocumentStartJavaScript( + const DocumentStartJavaScript* script, + content::RenderFrameHost* render_frame_host) { + DCHECK(script); + mojo::AssociatedRemote<mojom::JsCommunication> configurator_remote; + render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface( + &configurator_remote); + configurator_remote->AddDocumentStartScript( + mojom::DocumentStartJavaScript::New(script->script_id_, script->script_, + script->allowed_origin_rules_)); +} + +void JsCommunicationHost::NotifyFrameForRemoveDocumentStartJavaScript( + int32_t script_id, + content::RenderFrameHost* render_frame_host) { + mojo::AssociatedRemote<mojom::JsCommunication> configurator_remote; + render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface( + &configurator_remote); + configurator_remote->RemoveDocumentStartScript(script_id); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/browser/js_communication_host.h b/chromium/components/js_injection/browser/js_communication_host.h new file mode 100644 index 00000000000..3cd096d163e --- /dev/null +++ b/chromium/components/js_injection/browser/js_communication_host.h @@ -0,0 +1,110 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_JS_COMMUNICATION_HOST_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_JS_COMMUNICATION_HOST_H_ + +#include <memory> +#include <vector> + +#include "base/optional.h" +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "content/public/browser/web_contents_observer.h" + +namespace content { +class RenderFrameHost; +} // namespace content + +namespace js_injection { + +class OriginMatcher; +struct DocumentStartJavaScript; +struct JsObject; +class JsToBrowserMessaging; +class WebMessageHostFactory; + +// This class is 1:1 with WebContents, when AddWebMessageListener() is called, +// it stores the information in this class and send them to renderer side +// JsCommunication if there is any. When RenderFrameCreated() gets called, it +// needs to configure that new RenderFrame with the information stores in this +// class. +class JsCommunicationHost : public content::WebContentsObserver { + public: + explicit JsCommunicationHost(content::WebContents* web_contents); + ~JsCommunicationHost() override; + + // Captures the result of adding script. There are two possibilities when + // adding script: there was an error, in which case |error_message| is set, + // otherwise the add was successful and |script_id| is set. + struct AddScriptResult { + AddScriptResult(); + AddScriptResult(const AddScriptResult&); + AddScriptResult& operator=(const AddScriptResult&); + ~AddScriptResult(); + + base::Optional<std::string> error_message; + base::Optional<int> script_id; + }; + + // Native side AddDocumentStartJavaScript, returns an error message if the + // parameters didn't pass necessary checks. + AddScriptResult AddDocumentStartJavaScript( + const base::string16& script, + const std::vector<std::string>& allowed_origin_rules); + + bool RemoveDocumentStartJavaScript(int script_id); + + // Adds a new WebMessageHostFactory. For any urls that match + // |allowed_origin_rules|, |js_object_name| is registered as a JS object that + // can be used by script on the page to send and receive messages. Returns + // an empty string on success. On failure, the return string gives the error + // message. + base::string16 AddWebMessageHostFactory( + std::unique_ptr<WebMessageHostFactory> factory, + const base::string16& js_object_name, + const std::vector<std::string>& allowed_origin_rules); + + // Returns the factory previously registered under the specified name. + void RemoveWebMessageHostFactory(const base::string16& js_object_name); + + struct RegisteredFactory { + base::string16 js_name; + OriginMatcher allowed_origin_rules; + WebMessageHostFactory* factory = nullptr; + }; + + // Returns the registered factories. + std::vector<RegisteredFactory> GetWebMessageHostFactories(); + + // content::WebContentsObserver implementations + void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; + + private: + void NotifyFrameForWebMessageListener( + content::RenderFrameHost* render_frame_host); + void NotifyFrameForAllDocumentStartJavaScripts( + content::RenderFrameHost* render_frame_host); + void NotifyFrameForAddDocumentStartJavaScript( + const DocumentStartJavaScript* script, + content::RenderFrameHost* render_frame_host); + + void NotifyFrameForRemoveDocumentStartJavaScript( + int32_t script_id, + content::RenderFrameHost* render_frame_host); + + int32_t next_script_id_ = 0; + std::vector<DocumentStartJavaScript> scripts_; + std::vector<std::unique_ptr<JsObject>> js_objects_; + std::map<content::RenderFrameHost*, + std::vector<std::unique_ptr<JsToBrowserMessaging>>> + js_to_browser_messagings_; + + DISALLOW_COPY_AND_ASSIGN(JsCommunicationHost); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_JS_COMMUNICATION_HOST_H_ diff --git a/chromium/components/js_injection/browser/js_to_browser_messaging.cc b/chromium/components/js_injection/browser/js_to_browser_messaging.cc new file mode 100644 index 00000000000..e7ce36d7fd0 --- /dev/null +++ b/chromium/components/js_injection/browser/js_to_browser_messaging.cc @@ -0,0 +1,129 @@ +// Copyright 2019 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 "components/js_injection/browser/js_to_browser_messaging.h" + +#include "base/stl_util.h" +#include "components/js_injection/browser/web_message.h" +#include "components/js_injection/browser/web_message_host.h" +#include "components/js_injection/browser/web_message_host_factory.h" +#include "components/js_injection/browser/web_message_reply_proxy.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" +#include "third_party/blink/public/common/messaging/message_port_descriptor.h" +#include "url/origin.h" +#include "url/url_util.h" + +namespace js_injection { +namespace { + +// We want to pass a string "null" for local file schemes, to make it +// consistent to the Blink side SecurityOrigin serialization. When both +// setAllow{File,Universal}AccessFromFileURLs are false, Blink::SecurityOrigin +// will be serialized as string "null" for local file schemes, but when +// setAllowFileAccessFromFileURLs is true, Blink::SecurityOrigin will be +// serialized as the scheme, which will be inconsistentt to this place. In +// this case we want to let developer to know that local files are not safe, +// so we still pass "null". +std::string GetOriginString(const url::Origin& source_origin) { + return base::Contains(url::GetLocalSchemes(), source_origin.scheme()) + ? "null" + : source_origin.Serialize(); +} + +} // namespace + +class JsToBrowserMessaging::ReplyProxyImpl : public WebMessageReplyProxy { + public: + explicit ReplyProxyImpl( + mojo::PendingAssociatedRemote<mojom::BrowserToJsMessaging> + java_to_js_messaging) + : java_to_js_messaging_(std::move(java_to_js_messaging)) {} + ReplyProxyImpl(const ReplyProxyImpl&) = delete; + ReplyProxyImpl& operator=(const ReplyProxyImpl&) = delete; + ~ReplyProxyImpl() override = default; + + // WebMessageReplyProxy: + void PostMessage(std::unique_ptr<WebMessage> message) override { + java_to_js_messaging_->OnPostMessage(message->message); + } + + private: + mojo::AssociatedRemote<mojom::BrowserToJsMessaging> java_to_js_messaging_; +}; + +JsToBrowserMessaging::JsToBrowserMessaging( + content::RenderFrameHost* render_frame_host, + mojo::PendingAssociatedReceiver<mojom::JsToBrowserMessaging> receiver, + WebMessageHostFactory* factory, + const OriginMatcher& origin_matcher) + : render_frame_host_(render_frame_host), + connection_factory_(factory), + origin_matcher_(origin_matcher) { + receiver_.Bind(std::move(receiver)); +} + +JsToBrowserMessaging::~JsToBrowserMessaging() = default; + +void JsToBrowserMessaging::PostMessage( + const base::string16& message, + std::vector<blink::MessagePortDescriptor> ports) { + DCHECK(render_frame_host_); + + content::WebContents* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host_); + + if (!web_contents) + return; + + // |source_origin| has no race with this PostMessage call, because of + // associated mojo channel, the committed origin message and PostMessage are + // in sequence. + const url::Origin source_origin = + render_frame_host_->GetLastCommittedOrigin(); + + if (!origin_matcher_.Matches(source_origin)) + return; + + // SetBrowserToJsMessaging must be called before this. + DCHECK(reply_proxy_); + + if (!host_) { + const std::string origin_string = GetOriginString(source_origin); + const bool is_main_frame = + web_contents->GetMainFrame() == render_frame_host_; + + host_ = connection_factory_->CreateHost(origin_string, is_main_frame, + reply_proxy_.get()); +#if DCHECK_IS_ON() + origin_string_ = origin_string; + is_main_frame_ = is_main_frame; +#endif + if (!host_) + return; + } + // The origin and whether this is the main frame should not change once + // PostMessage() has been received. +#if DCHECK_IS_ON() + DCHECK_EQ(GetOriginString(source_origin), origin_string_); + DCHECK_EQ(is_main_frame_, web_contents->GetMainFrame() == render_frame_host_); +#endif + std::unique_ptr<WebMessage> web_message = std::make_unique<WebMessage>(); + web_message->message = message; + web_message->ports = std::move(ports); + host_->OnPostMessage(std::move(web_message)); +} + +void JsToBrowserMessaging::SetBrowserToJsMessaging( + mojo::PendingAssociatedRemote<mojom::BrowserToJsMessaging> + java_to_js_messaging) { + // A RenderFrame may inject JsToBrowserMessaging in the JavaScript context + // more than once because of reusing of RenderFrame. + host_.reset(); + reply_proxy_ = + std::make_unique<ReplyProxyImpl>(std::move(java_to_js_messaging)); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/browser/js_to_browser_messaging.h b/chromium/components/js_injection/browser/js_to_browser_messaging.h new file mode 100644 index 00000000000..716905182f1 --- /dev/null +++ b/chromium/components/js_injection/browser/js_to_browser_messaging.h @@ -0,0 +1,66 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_JS_TO_BROWSER_MESSAGING_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_JS_TO_BROWSER_MESSAGING_H_ + +#include <vector> + +#include "base/check.h" +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "components/js_injection/common/origin_matcher.h" +#include "mojo/public/cpp/bindings/associated_receiver_set.h" +#include "mojo/public/cpp/bindings/associated_remote.h" +#include "mojo/public/cpp/bindings/pending_associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_associated_remote.h" +#include "third_party/blink/public/common/messaging/message_port_descriptor.h" + +namespace content { +class RenderFrameHost; +} + +namespace js_injection { + +class WebMessageHost; +class WebMessageHostFactory; + +// Implementation of mojo::JsToBrowserMessaging interface. Receives +// PostMessage() call from renderer JsBinding. +class JsToBrowserMessaging : public mojom::JsToBrowserMessaging { + public: + JsToBrowserMessaging( + content::RenderFrameHost* rfh, + mojo::PendingAssociatedReceiver<mojom::JsToBrowserMessaging> receiver, + WebMessageHostFactory* factory, + const OriginMatcher& origin_matcher); + ~JsToBrowserMessaging() override; + + // mojom::JsToBrowserMessaging implementation. + void PostMessage(const base::string16& message, + std::vector<blink::MessagePortDescriptor> ports) override; + void SetBrowserToJsMessaging( + mojo::PendingAssociatedRemote<mojom::BrowserToJsMessaging> + java_to_js_messaging) override; + + private: + class ReplyProxyImpl; + + content::RenderFrameHost* render_frame_host_; + std::unique_ptr<ReplyProxyImpl> reply_proxy_; + WebMessageHostFactory* connection_factory_; + OriginMatcher origin_matcher_; + mojo::AssociatedReceiver<mojom::JsToBrowserMessaging> receiver_{this}; + std::unique_ptr<WebMessageHost> host_; +#if DCHECK_IS_ON() + std::string origin_string_; + bool is_main_frame_; +#endif + + DISALLOW_COPY_AND_ASSIGN(JsToBrowserMessaging); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_JS_TO_BROWSER_MESSAGING_H_ diff --git a/chromium/components/js_injection/browser/web_message.cc b/chromium/components/js_injection/browser/web_message.cc new file mode 100644 index 00000000000..34e8e64e08f --- /dev/null +++ b/chromium/components/js_injection/browser/web_message.cc @@ -0,0 +1,13 @@ +// Copyright 2020 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 "components/js_injection/browser/web_message.h" + +namespace js_injection { + +WebMessage::WebMessage() = default; + +WebMessage::~WebMessage() = default; + +} // namespace js_injection diff --git a/chromium/components/js_injection/browser/web_message.h b/chromium/components/js_injection/browser/web_message.h new file mode 100644 index 00000000000..1061cc82a76 --- /dev/null +++ b/chromium/components/js_injection/browser/web_message.h @@ -0,0 +1,26 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "third_party/blink/public/common/messaging/message_port_descriptor.h" + +namespace js_injection { + +// Represents a message to or from the page. +struct WebMessage { + WebMessage(); + ~WebMessage(); + + base::string16 message; + std::vector<blink::MessagePortDescriptor> ports; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_H_ diff --git a/chromium/components/js_injection/browser/web_message_host.h b/chromium/components/js_injection/browser/web_message_host.h new file mode 100644 index 00000000000..8fa339893fa --- /dev/null +++ b/chromium/components/js_injection/browser/web_message_host.h @@ -0,0 +1,24 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_H_ + +#include <memory> + +namespace js_injection { + +struct WebMessage; + +// Represents the browser side of a WebMessage channel. +class WebMessageHost { + public: + virtual ~WebMessageHost() = default; + + virtual void OnPostMessage(std::unique_ptr<WebMessage> message) = 0; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_H_ diff --git a/chromium/components/js_injection/browser/web_message_host_factory.h b/chromium/components/js_injection/browser/web_message_host_factory.h new file mode 100644 index 00000000000..aeaaf772bc2 --- /dev/null +++ b/chromium/components/js_injection/browser/web_message_host_factory.h @@ -0,0 +1,34 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_FACTORY_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_FACTORY_H_ + +#include <memory> +#include <string> + +namespace js_injection { + +class WebMessageHost; +class WebMessageReplyProxy; + +// Creates a WebMessageHost in response to a page interacting with the object +// registered by way of JsCommunicationHost::AddWebMessageHostFactory(). A +// WebMessageHost is created for every page that matches the parameters of +// AddWebMessageHostFactory(). +class WebMessageHostFactory { + public: + virtual ~WebMessageHostFactory() = default; + + // Creates a WebMessageHost for the specified page. |proxy| is valid for + // the life of the host and may be used to send messages back to the page. + virtual std::unique_ptr<WebMessageHost> CreateHost( + const std::string& origin_string, + bool is_main_frame, + WebMessageReplyProxy* proxy) = 0; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_HOST_FACTORY_H_ diff --git a/chromium/components/js_injection/browser/web_message_reply_proxy.h b/chromium/components/js_injection/browser/web_message_reply_proxy.h new file mode 100644 index 00000000000..e94171f221f --- /dev/null +++ b/chromium/components/js_injection/browser/web_message_reply_proxy.h @@ -0,0 +1,25 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_REPLY_PROXY_H_ +#define COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_REPLY_PROXY_H_ + +#include "base/strings/string16.h" + +namespace js_injection { + +struct WebMessage; + +// Used to send messages to the page. +class WebMessageReplyProxy { + public: + virtual void PostMessage(std::unique_ptr<WebMessage> message) = 0; + + protected: + virtual ~WebMessageReplyProxy() = default; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_BROWSER_WEB_MESSAGE_REPLY_PROXY_H_ diff --git a/chromium/components/js_injection/common/BUILD.gn b/chromium/components/js_injection/common/BUILD.gn new file mode 100644 index 00000000000..4c444214440 --- /dev/null +++ b/chromium/components/js_injection/common/BUILD.gn @@ -0,0 +1,71 @@ +# Copyright 2020 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. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("common_mojom") { + sources = [ + "interfaces.mojom", + "origin_matcher.mojom", + ] + + public_deps = [ + "//mojo/public/mojom/base", + "//third_party/blink/public/mojom:mojom_core", + ] + + cpp_typemaps = [ + { + types = [ + { + mojom = "js_injection.mojom.OriginMatcher" + cpp = "::js_injection::OriginMatcher" + }, + { + mojom = "js_injection.mojom.OriginMatcherRule" + cpp = "::std::unique_ptr<::js_injection::OriginMatcherRule>" + move_only = true + }, + ] + traits_headers = [ + "origin_matcher_mojom_traits.h", + "origin_matcher.h", + ] + traits_sources = [ "origin_matcher_mojom_traits.cc" ] + traits_public_deps = [ ":common" ] + }, + ] + overridden_deps = [ "//third_party/blink/public/mojom:mojom_core" ] + component_deps = [ "//third_party/blink/public/common" ] +} + +source_set("common") { + public = [ "origin_matcher.h" ] + sources = [ + "origin_matcher.cc", + "origin_matcher_internal.cc", + "origin_matcher_internal.h", + ] + deps = [ + "//base", + "//net", + "//url", + ] + + # origin_matcher_internal is needed by mojom traits and tests. + friend = [ ":*" ] +} + +source_set("unit_tests") { + testonly = true + sources = [ "origin_matcher_unittest.cc" ] + deps = [ + ":common", + ":common_mojom", + "//base", + "//base/test:test_support", + "//mojo/public/cpp/test_support:test_utils", + "//url", + ] +} diff --git a/chromium/components/js_injection/common/OWNERS b/chromium/components/js_injection/common/OWNERS new file mode 100644 index 00000000000..b36774dad1d --- /dev/null +++ b/chromium/components/js_injection/common/OWNERS @@ -0,0 +1,4 @@ +per-file *_mojom_traits*.*=set noparent +per-file *_mojom_traits*.*=file://ipc/SECURITY_OWNERS +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS diff --git a/chromium/components/js_injection/common/interfaces.mojom b/chromium/components/js_injection/common/interfaces.mojom new file mode 100644 index 00000000000..0e5b1047f91 --- /dev/null +++ b/chromium/components/js_injection/common/interfaces.mojom @@ -0,0 +1,65 @@ +// Copyright 2019 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. + +module js_injection.mojom; + +import "components/js_injection/common/origin_matcher.mojom"; +import "mojo/public/mojom/base/string16.mojom"; +import "third_party/blink/public/mojom/messaging/message_port_descriptor.mojom"; + +// JsObject struct represents a JavaScript object we will inject in the main +// JavaScript world of a frame. |js_object_name| will be used as the name +// of the JavaScript object. We will inject the object if the frame's origin +// matches |origin_matcher|. |js_to_browser_messaging| will be used for that +// JavaScript object to send message back to browser side. +struct JsObject { + mojo_base.mojom.String16 js_object_name; + pending_associated_remote<JsToBrowserMessaging> js_to_browser_messaging; + js_injection.mojom.OriginMatcher origin_matcher; +}; + +// DocumentStartJavaScript struct contains the JavaScript snippet |script| and +// the corresponding |origin_matcher|. We will run the script if the frame's +// origin matches any rules in the |origin_matcher|. +struct DocumentStartJavaScript { + int32 script_id; + mojo_base.mojom.String16 script; + js_injection.mojom.OriginMatcher origin_matcher; +}; + +// For JavaScript postMessage() API, implemented by browser. +interface JsToBrowserMessaging { + // Called from renderer, browser receives |message| and possible |ports|, + // The |message| is an opaque type and the contents are defined by the client + // of this API. + PostMessage(mojo_base.mojom.String16 message, + array<blink.mojom.MessagePortDescriptor> ports); + + // When there is a new BrowserToJsMessaging created in renderer, we need to + // send/ it to browser, so browser could send message back to Js. + SetBrowserToJsMessaging( + pending_associated_remote<BrowserToJsMessaging> browser_to_js_messaging); +}; + +// For the browser to reply back to injected JavaScript object. Implemented by +// the renderer. +interface BrowserToJsMessaging { + // Called from browser, to send message to page. + OnPostMessage(mojo_base.mojom.String16 message); +}; + +// For browser to configure renderer, implemented by renderer. +interface JsCommunication { + // Called from browser, to tell renderer that if we need to inject + // JavaScript objects to the frame based on the |js_objects| array. + SetJsObjects(array<js_injection.mojom.JsObject> js_objects); + + // Called from browser, to add a script for a frame to run at document start + // stage. The script will run only if the frame's origin matches any of the + // allowed_origin_rules. + AddDocumentStartScript(js_injection.mojom.DocumentStartJavaScript script); + + // Called from browser, to remove the script by the given script_id. + RemoveDocumentStartScript(int32 script_id); +}; diff --git a/chromium/components/js_injection/common/origin_matcher.cc b/chromium/components/js_injection/common/origin_matcher.cc new file mode 100644 index 00000000000..bdfc9fa3eaf --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher.cc @@ -0,0 +1,125 @@ +// Copyright 2020 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 "components/js_injection/common/origin_matcher.h" + +#include "components/js_injection/common/origin_matcher_internal.h" +#include "net/base/ip_address.h" +#include "net/base/ip_endpoint.h" +#include "net/base/parse_number.h" +#include "net/base/url_util.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_constants.h" +#include "url/url_util.h" + +namespace js_injection { + +namespace { + +inline int GetDefaultPortForSchemeIfNoPortInfo(const std::string& scheme, + int port) { + // The input has explicit port information, so don't modify it. + if (port != -1) + return port; + + // Hard code the port for http and https. + if (scheme == url::kHttpScheme) + return 80; + if (scheme == url::kHttpsScheme) + return 443; + + return port; +} + +} // namespace + +OriginMatcher::OriginMatcher(const OriginMatcher& rhs) { + *this = rhs; +} + +OriginMatcher& OriginMatcher::operator=(const OriginMatcher& rhs) { + rules_.clear(); + for (const auto& rule : rhs.Serialize()) + AddRuleFromString(rule); + return *this; +} + +void OriginMatcher::SetRules(RuleList rules) { + rules_.swap(rules); +} + +bool OriginMatcher::AddRuleFromString(const std::string& raw_untrimmed) { + std::string raw; + base::TrimWhitespaceASCII(raw_untrimmed, base::TRIM_ALL, &raw); + + if (raw == "*") { + rules_.push_back(std::make_unique<MatchAllOriginsRule>()); + return true; + } + + // Extract scheme-restriction. + std::string::size_type scheme_pos = raw.find("://"); + if (scheme_pos == std::string::npos) + return false; + + const std::string scheme = raw.substr(0, scheme_pos); + if (!SubdomainMatchingRule::IsValidScheme(scheme)) + return false; + + std::string host_and_port = raw.substr(scheme_pos + 3); + if (host_and_port.empty()) { + if (!SubdomainMatchingRule::IsValidSchemeAndHost(scheme, std::string())) + return false; + rules_.push_back( + std::make_unique<SubdomainMatchingRule>(scheme, std::string(), -1)); + return true; + } + + std::string host; + int port; + if (!net::ParseHostAndPort(host_and_port, &host, &port) || + !SubdomainMatchingRule::IsValidSchemeAndHost(scheme, host)) { + return false; + } + + // Check if we have an <ip-address>[:port] input and try to canonicalize the + // IP literal. + net::IPAddress ip_address; + if (ip_address.AssignFromIPLiteral(host)) { + port = GetDefaultPortForSchemeIfNoPortInfo(scheme, port); + host = ip_address.ToString(); + if (ip_address.IsIPv6()) + host = '[' + host + ']'; + rules_.push_back( + std::make_unique<SubdomainMatchingRule>(scheme, host, port)); + return true; + } + + port = GetDefaultPortForSchemeIfNoPortInfo(scheme, port); + rules_.push_back(std::make_unique<SubdomainMatchingRule>(scheme, host, port)); + return true; +} + +bool OriginMatcher::Matches(const url::Origin& origin) const { + GURL origin_url = origin.GetURL(); + // Since we only do kInclude vs kNoMatch, the order doesn't actually matter. + for (auto it = rules_.rbegin(); it != rules_.rend(); ++it) { + net::SchemeHostPortMatcherResult result = (*it)->Evaluate(origin_url); + if (result == net::SchemeHostPortMatcherResult::kInclude) + return true; + } + return false; +} + +std::vector<std::string> OriginMatcher::Serialize() const { + std::vector<std::string> result; + result.reserve(rules_.size()); + for (const auto& rule : rules_) { + result.push_back(rule->ToString()); + } + return result; +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/common/origin_matcher.h b/chromium/components/js_injection/common/origin_matcher.h new file mode 100644 index 00000000000..08f176042b6 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher.h @@ -0,0 +1,75 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_H_ +#define COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_H_ + +#include <memory> +#include <string> +#include <vector> + +namespace url { +class Origin; +} // namespace url + +namespace js_injection { + +class OriginMatcherRule; + +// An url origin matcher allows wildcard subdomain matching. It supports two +// types of rules. +// +// (1) "*" +// A single * (without quote) will match any origin. +// +// (2) SCHEME "://" [ HOSTNAME_PATTERN ][":" PORT] +// +// SCHEME is required. When matching custom schemes, HOSTNAME_PATTERN and PORT +// shouldn't present. When SCHEME is "http" or "https", HOSTNAME_PATTERN is +// required. +// +// HOSTNAME_PATTERN allows wildcard '*' to match subdomains, such as +// "*.example.com". Rules such as "x.*.y.com", "*foobar.com" are not allowed. +// Note that "*.example.com" won't match "example.com", so need another rule +// "example.com" to match it. If the HOSTNAME_PATTERN is an IP literal, it +// will be used for exact matching. +// +// PORT is optional for "http" and "https" schemes, when it is not present, for +// "http" and "https" schemes, it will match default port number (80 and 443 +// correspondingly). +class OriginMatcher { + public: + using RuleList = std::vector<std::unique_ptr<OriginMatcherRule>>; + + OriginMatcher() = default; + // Allow copy and assign. + OriginMatcher(const OriginMatcher& rhs); + OriginMatcher(OriginMatcher&&) = default; + OriginMatcher& operator=(const OriginMatcher& rhs); + OriginMatcher& operator=(OriginMatcher&&) = default; + + ~OriginMatcher() = default; + + void SetRules(RuleList rules); + + // Adds a rule given by the string |raw|. Returns true if the rule was + // successfully added. + bool AddRuleFromString(const std::string& raw); + + // Returns true if the |origin| matches any rule in this matcher. + bool Matches(const url::Origin& origin) const; + + // Returns the current list of rules. + const RuleList& rules() const { return rules_; } + + // Returns string representation of this origin matcher. + std::vector<std::string> Serialize() const; + + private: + RuleList rules_; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_H_ diff --git a/chromium/components/js_injection/common/origin_matcher.mojom b/chromium/components/js_injection/common/origin_matcher.mojom new file mode 100644 index 00000000000..1461812adb0 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher.mojom @@ -0,0 +1,22 @@ +// Copyright 2020 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. + +module js_injection.mojom; + +// Values correspond to that of SubdomainMatchingRule. +struct SubdomainMatchingRule { + string scheme; + // An empty host matches any host. + string optional_host; + int32 optional_port; +}; + +struct OriginMatcherRule { + // If this is not set, the rule matches any url. + SubdomainMatchingRule? subdomain_matching_rule; +}; + +struct OriginMatcher { + array<OriginMatcherRule> rules; +}; diff --git a/chromium/components/js_injection/common/origin_matcher_internal.cc b/chromium/components/js_injection/common/origin_matcher_internal.cc new file mode 100644 index 00000000000..0630b5f9e72 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher_internal.cc @@ -0,0 +1,125 @@ +// Copyright 2020 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 "components/js_injection/common/origin_matcher_internal.h" + +#include "base/strings/pattern.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/base/scheme_host_port_matcher_rule.h" +#include "net/base/url_util.h" +#include "url/gurl.h" +#include "url/url_constants.h" + +namespace js_injection { +namespace { + +// Returns false if |host| has too many wildcards. +inline bool HostWildcardSanityCheck(const std::string& host) { + size_t wildcard_count = std::count(host.begin(), host.end(), '*'); + if (wildcard_count == 0) + return true; + + // We only allow one wildcard. + if (wildcard_count > 1) + return false; + + // Start with "*." for subdomain matching. + if (base::StartsWith(host, "*.", base::CompareCase::SENSITIVE)) + return true; + + return false; +} + +} // namespace + +OriginMatcherRule::OriginMatcherRule(OriginMatcherRuleType type) + : type_(type) {} + +OriginMatcherRule::~OriginMatcherRule() = default; + +MatchAllOriginsRule::MatchAllOriginsRule() + : OriginMatcherRule(OriginMatcherRuleType::kAny) {} + +MatchAllOriginsRule::~MatchAllOriginsRule() = default; + +net::SchemeHostPortMatcherResult MatchAllOriginsRule::Evaluate( + const GURL& url) const { + return net::SchemeHostPortMatcherResult::kInclude; +} + +std::string MatchAllOriginsRule::ToString() const { + return "*"; +} + +SubdomainMatchingRule::SubdomainMatchingRule(const std::string& scheme, + const std::string& optional_host, + int optional_port) + : OriginMatcherRule(OriginMatcherRuleType::kSubdomain), + scheme_(base::ToLowerASCII(scheme)), + optional_host_(base::ToLowerASCII(optional_host)), + optional_port_(optional_port) { + DCHECK(IsValidScheme(scheme)); + DCHECK(IsValidSchemeAndHost(scheme_, optional_host_)); +} + +SubdomainMatchingRule::~SubdomainMatchingRule() = default; + +// static +bool SubdomainMatchingRule::IsValidScheme(const std::string& scheme) { + // Wild cards are not allowed in the scheme. + return !scheme.empty() && scheme.find('*') == std::string::npos; +} + +// static +bool SubdomainMatchingRule::CanSchemeHaveHost(const std::string& scheme) { + return scheme == url::kHttpScheme || scheme == url::kHttpsScheme; +} + +// static +bool SubdomainMatchingRule::IsValidSchemeAndHost(const std::string& scheme, + const std::string& host) { + if (host.empty()) { + if (CanSchemeHaveHost(scheme)) + return false; + return true; + } + if (!CanSchemeHaveHost(scheme)) + return false; + + // |scheme| is either https or http. + + // URL like rule is invalid. + if (host.find('/') != std::string::npos) + return false; + + return HostWildcardSanityCheck(host); +} + +net::SchemeHostPortMatcherResult SubdomainMatchingRule::Evaluate( + const GURL& url) const { + if (optional_port_ != -1 && url.EffectiveIntPort() != optional_port_) { + // Didn't match port expectation. + return net::SchemeHostPortMatcherResult::kNoMatch; + } + + if (url.scheme() != scheme_) { + // Didn't match scheme expectation. + return net::SchemeHostPortMatcherResult::kNoMatch; + } + + return base::MatchPattern(url.host(), optional_host_) + ? net::SchemeHostPortMatcherResult::kInclude + : net::SchemeHostPortMatcherResult::kNoMatch; +} + +std::string SubdomainMatchingRule::ToString() const { + std::string str; + base::StringAppendF(&str, "%s://%s", scheme_.c_str(), optional_host_.c_str()); + if (optional_port_ != -1) + base::StringAppendF(&str, ":%d", optional_port_); + return str; +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/common/origin_matcher_internal.h b/chromium/components/js_injection/common/origin_matcher_internal.h new file mode 100644 index 00000000000..b9661bfa7e9 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher_internal.h @@ -0,0 +1,89 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_INTERNAL_H_ +#define COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_INTERNAL_H_ + +#include <string> + +#include "net/base/scheme_host_port_matcher.h" + +// NOTE: this file is an implementation detail and only used by code in +// js_injection. + +namespace js_injection { + +enum class OriginMatcherRuleType { kAny, kSubdomain }; + +// Common superclass that includes the type of matcher. +class OriginMatcherRule : public net::SchemeHostPortMatcherRule { + public: + explicit OriginMatcherRule(OriginMatcherRuleType type); + ~OriginMatcherRule() override; + + OriginMatcherRuleType type() const { return type_; } + + private: + const OriginMatcherRuleType type_; +}; + +// Matches *all* urls. +class MatchAllOriginsRule : public OriginMatcherRule { + public: + MatchAllOriginsRule(); + MatchAllOriginsRule(const MatchAllOriginsRule&) = delete; + MatchAllOriginsRule& operator=(const MatchAllOriginsRule&) = delete; + ~MatchAllOriginsRule() override; + + // OriginMatcherRule: + net::SchemeHostPortMatcherResult Evaluate(const GURL& url) const override; + std::string ToString() const override; +}; + +// Matches against a specific scheme, optional host (potentially with +// wild-cards) and an optional port. +class SubdomainMatchingRule : public OriginMatcherRule { + public: + SubdomainMatchingRule(const std::string& scheme, + const std::string& optional_host, + int optional_port); + // This constructor is implemented only in tests, it does no checking of + // args. + SubdomainMatchingRule(const std::string& scheme, + const std::string& optional_host, + int optional_port, + bool for_test); + SubdomainMatchingRule(const SubdomainMatchingRule&) = delete; + SubdomainMatchingRule& operator=(const SubdomainMatchingRule&) = delete; + ~SubdomainMatchingRule() override; + + // Returns true if |scheme| is a valid scheme identifier. + static bool IsValidScheme(const std::string& scheme); + + // Returns true if the |scheme| is allowed to have a host and port part. + static bool CanSchemeHaveHost(const std::string& scheme); + + // Returns true if |scheme| and |host| are valid. + static bool IsValidSchemeAndHost(const std::string& scheme, + const std::string& host); + + const std::string& scheme() const { return scheme_; } + const std::string& optional_host() const { return optional_host_; } + int optional_port() const { return optional_port_; } + + // OriginMatcherRule: + net::SchemeHostPortMatcherResult Evaluate(const GURL& url) const override; + std::string ToString() const override; + + private: + const std::string scheme_; + // Empty string means no host provided. + const std::string optional_host_; + // -1 means no port provided. + const int optional_port_; +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_INTERNAL_H_ diff --git a/chromium/components/js_injection/common/origin_matcher_mojom_traits.cc b/chromium/components/js_injection/common/origin_matcher_mojom_traits.cc new file mode 100644 index 00000000000..3bb7bd838a0 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher_mojom_traits.cc @@ -0,0 +1,85 @@ +// Copyright 2020 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 "components/js_injection/common/origin_matcher_mojom_traits.h" + +#include "base/strings/pattern.h" +#include "base/strings/stringprintf.h" +#include "components/js_injection/common/origin_matcher_internal.h" +#include "net/base/ip_address.h" +#include "net/base/ip_endpoint.h" +#include "net/base/parse_number.h" +#include "net/base/scheme_host_port_matcher_rule.h" +#include "net/base/url_util.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_constants.h" +#include "url/url_util.h" + +namespace mojo { + +using js_injection::SubdomainMatchingRule; +using js_injection::mojom::OriginMatcherRuleDataView; + +// static +js_injection::mojom::SubdomainMatchingRulePtr +StructTraits<OriginMatcherRuleDataView, OriginMatcherRuleUniquePtr>:: + subdomain_matching_rule(const OriginMatcherRuleUniquePtr& rule) { + if (rule->type() == js_injection::OriginMatcherRuleType::kAny) + return nullptr; + + DCHECK_EQ(js_injection::OriginMatcherRuleType::kSubdomain, rule->type()); + const SubdomainMatchingRule* matching_rule = + static_cast<SubdomainMatchingRule*>(rule.get()); + js_injection::mojom::SubdomainMatchingRulePtr matching_rule_ptr( + js_injection::mojom::SubdomainMatchingRule::New()); + + matching_rule_ptr->scheme = matching_rule->scheme(); + matching_rule_ptr->optional_host = matching_rule->optional_host(); + matching_rule_ptr->optional_port = matching_rule->optional_port(); + return matching_rule_ptr; +} + +// static +bool StructTraits<OriginMatcherRuleDataView, OriginMatcherRuleUniquePtr>::Read( + OriginMatcherRuleDataView r, + OriginMatcherRuleUniquePtr* out) { + DCHECK(!out->get()); + + js_injection::mojom::SubdomainMatchingRuleDataView + subdomain_matching_rule_data_view; + r.GetSubdomainMatchingRuleDataView(&subdomain_matching_rule_data_view); + if (subdomain_matching_rule_data_view.is_null()) { + *out = std::make_unique<js_injection::MatchAllOriginsRule>(); + return true; + } + + js_injection::mojom::SubdomainMatchingRulePtr subdomain_matching_rule; + if (!r.ReadSubdomainMatchingRule(&subdomain_matching_rule)) + return false; + if (!SubdomainMatchingRule::IsValidScheme(subdomain_matching_rule->scheme) || + !SubdomainMatchingRule::IsValidSchemeAndHost( + subdomain_matching_rule->scheme, + subdomain_matching_rule->optional_host)) { + return false; + } + *out = std::make_unique<SubdomainMatchingRule>( + subdomain_matching_rule->scheme, subdomain_matching_rule->optional_host, + subdomain_matching_rule->optional_port); + return true; +} + +// static +bool StructTraits<js_injection::mojom::OriginMatcherDataView, + js_injection::OriginMatcher>:: + Read(js_injection::mojom::OriginMatcherDataView data, + js_injection::OriginMatcher* out) { + std::vector<OriginMatcherRuleUniquePtr> rules; + if (!data.ReadRules(&rules)) + return false; + out->SetRules(std::move(rules)); + return true; +} + +} // namespace mojo diff --git a/chromium/components/js_injection/common/origin_matcher_mojom_traits.h b/chromium/components/js_injection/common/origin_matcher_mojom_traits.h new file mode 100644 index 00000000000..873d2cb94e2 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher_mojom_traits.h @@ -0,0 +1,45 @@ +// Copyright 2020 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. + +#ifndef COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_MOJOM_TRAITS_H_ +#define COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_MOJOM_TRAITS_H_ + +#include <string> +#include <vector> + +#include "components/js_injection/common/origin_matcher.h" +#include "components/js_injection/common/origin_matcher.mojom.h" +#include "components/js_injection/common/origin_matcher_internal.h" +#include "mojo/public/cpp/bindings/struct_traits.h" + +namespace mojo { + +using OriginMatcherRuleUniquePtr = + std::unique_ptr<js_injection::OriginMatcherRule>; + +template <> +struct StructTraits<js_injection::mojom::OriginMatcherRuleDataView, + OriginMatcherRuleUniquePtr> { + static js_injection::mojom::SubdomainMatchingRulePtr subdomain_matching_rule( + const OriginMatcherRuleUniquePtr& rule); + static bool Read(js_injection::mojom::OriginMatcherRuleDataView r, + OriginMatcherRuleUniquePtr* out); +}; + +template <> +struct StructTraits<js_injection::mojom::OriginMatcherDataView, + js_injection::OriginMatcher> { + public: + static const std::vector<OriginMatcherRuleUniquePtr>& rules( + const js_injection::OriginMatcher& r) { + return r.rules(); + } + + static bool Read(js_injection::mojom::OriginMatcherDataView data, + js_injection::OriginMatcher* out); +}; + +} // namespace mojo + +#endif // COMPONENTS_JS_INJECTION_COMMON_ORIGIN_MATCHER_MOJOM_TRAITS_H_ diff --git a/chromium/components/js_injection/common/origin_matcher_unittest.cc b/chromium/components/js_injection/common/origin_matcher_unittest.cc new file mode 100644 index 00000000000..ab7564ef210 --- /dev/null +++ b/chromium/components/js_injection/common/origin_matcher_unittest.cc @@ -0,0 +1,404 @@ +// Copyright 2020 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 "components/js_injection/common/origin_matcher.h" + +#include "components/js_injection/common/origin_matcher.mojom.h" +#include "components/js_injection/common/origin_matcher_internal.h" +#include "mojo/public/cpp/test_support/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_util.h" + +namespace js_injection { + +SubdomainMatchingRule::SubdomainMatchingRule(const std::string& scheme, + const std::string& optional_host, + int optional_port, + bool for_test) + : OriginMatcherRule(OriginMatcherRuleType::kSubdomain), + scheme_(scheme), + optional_host_(optional_host), + optional_port_(optional_port) {} + +class OriginMatcherTest : public testing::Test { + public: + void SetUp() override { + scheme_registry_ = std::make_unique<url::ScopedSchemeRegistryForTests>(); + url::EnableNonStandardSchemesForAndroidWebView(); + } + + static url::Origin CreateOriginFromString(const std::string& url) { + return url::Origin::Create(GURL(url)); + } + + private: + std::unique_ptr<url::ScopedSchemeRegistryForTests> scheme_registry_; +}; + +TEST_F(OriginMatcherTest, InvalidInputs) { + OriginMatcher matcher; + // Empty string is invalid. + EXPECT_FALSE(matcher.AddRuleFromString("")); + // Scheme doesn't present. + EXPECT_FALSE(matcher.AddRuleFromString("example.com")); + EXPECT_FALSE(matcher.AddRuleFromString("://example.com")); + // Scheme doesn't do wildcard matching. + EXPECT_FALSE(matcher.AddRuleFromString("*://example.com")); + // URL like rule is invalid. + EXPECT_FALSE(matcher.AddRuleFromString("https://www.example.com/index.html")); + EXPECT_FALSE(matcher.AddRuleFromString("http://192.168.0.1/*")); + // Only accept hostname pattern starts with "*." if there is a "*" inside. + EXPECT_FALSE(matcher.AddRuleFromString("https://*foobar.com")); + EXPECT_FALSE(matcher.AddRuleFromString("https://x.*.y.com")); + EXPECT_FALSE(matcher.AddRuleFromString("https://*example.com")); + EXPECT_FALSE(matcher.AddRuleFromString("https://e*xample.com")); + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com*")); + EXPECT_FALSE(matcher.AddRuleFromString("https://*")); + EXPECT_FALSE(matcher.AddRuleFromString("http://*")); + // Invalid port. + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com:")); + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com:*")); + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com:**")); + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com:-1")); + EXPECT_FALSE(matcher.AddRuleFromString("https://example.com:+443")); + // Empty hostname pattern for http/https. + EXPECT_FALSE(matcher.AddRuleFromString("http://")); + EXPECT_FALSE(matcher.AddRuleFromString("https://")); + EXPECT_FALSE(matcher.AddRuleFromString("https://:80")); + // No IP block support. + EXPECT_FALSE(matcher.AddRuleFromString("https://192.168.0.0/16")); + EXPECT_FALSE(matcher.AddRuleFromString("https://fefe:13::abc/33")); + EXPECT_FALSE(matcher.AddRuleFromString("https://:1")); + // Invalid IP address. + EXPECT_FALSE(matcher.AddRuleFromString("http://[a:b:*]")); + EXPECT_FALSE(matcher.AddRuleFromString("http://[a:b:*")); + EXPECT_FALSE(matcher.AddRuleFromString("https://fefe:13::*")); + EXPECT_FALSE(matcher.AddRuleFromString("https://fefe:13:*/33")); + // Custom scheme with host and/or port are invalid. This is because in + // WebView, all the URI with the same custom scheme belong to one origin. + EXPECT_FALSE(matcher.AddRuleFromString("x-mail://hostname:80")); + EXPECT_FALSE(matcher.AddRuleFromString("x-mail://hostname")); + EXPECT_FALSE(matcher.AddRuleFromString("x-mail://*")); + // file scheme with "host" + EXPECT_FALSE(matcher.AddRuleFromString("file://host")); + EXPECT_FALSE(matcher.AddRuleFromString("file://*")); +} + +TEST_F(OriginMatcherTest, ExactMatching) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("https://www.example.com:99")); + EXPECT_EQ("https://www.example.com:99", matcher.rules()[0]->ToString()); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com:99"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://www.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://music.example.com:99"))); +} + +TEST_F(OriginMatcherTest, SchemeDefaultPortHttp) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("http://www.example.com")); + EXPECT_EQ("http://www.example.com:80", matcher.rules()[0]->ToString()); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://www.example.com:80"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://music.example.com:80"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://music.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://www.example.com:80"))); +} + +TEST_F(OriginMatcherTest, SchemeDefaultPortHttps) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("https://www.example.com")); + EXPECT_EQ("https://www.example.com:443", matcher.rules()[0]->ToString()); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com:443"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com:443"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://music.example.com:99"))); +} + +TEST_F(OriginMatcherTest, SubdomainMatching) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("https://*.example.com")); + EXPECT_EQ("https://*.example.com:443", matcher.rules()[0]->ToString()); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com:443"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://music.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://music.example.com:443"))); + EXPECT_TRUE(matcher.Matches( + CreateOriginFromString("https://music.video.radio.example.com"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("ftp://www.example.com"))); + EXPECT_FALSE(matcher.Matches(CreateOriginFromString("https://example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://music.example.com:99"))); +} + +TEST_F(OriginMatcherTest, SubdomainMatching2) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("http://*.www.example.com")); + EXPECT_EQ("http://*.www.example.com:80", matcher.rules()[0]->ToString()); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://www.www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://abc.www.example.com:80"))); + EXPECT_TRUE(matcher.Matches( + CreateOriginFromString("http://music.video.www.example.com"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("ftp://www.example.com"))); + EXPECT_FALSE(matcher.Matches(CreateOriginFromString("https://example.com"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://www.example.com:99"))); + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://music.example.com:99"))); +} + +TEST_F(OriginMatcherTest, PunyCode) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("http://*.xn--fsqu00a.com")); + + // Chinese domain example.com + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("http://www.例子.com"))); +} + +TEST_F(OriginMatcherTest, IPv4AddressMatching) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("https://192.168.0.1")); + EXPECT_EQ("https://192.168.0.1:443", matcher.rules()[0]->ToString()); + + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("https://192.168.0.1"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://192.168.0.1:443"))); + + EXPECT_FALSE( + matcher.Matches(CreateOriginFromString("https://192.168.0.1:99"))); + EXPECT_FALSE(matcher.Matches(CreateOriginFromString("http://192.168.0.1"))); + EXPECT_FALSE(matcher.Matches(CreateOriginFromString("http://192.168.0.2"))); +} + +TEST_F(OriginMatcherTest, IPv6AddressMatching) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("https://[3ffe:2a00:100:7031:0:0::1]")); + // Note that the IPv6 address is canonicalized. + EXPECT_EQ("https://[3ffe:2a00:100:7031::1]:443", + matcher.rules()[0]->ToString()); + + EXPECT_TRUE(matcher.Matches( + CreateOriginFromString("https://[3ffe:2a00:100:7031::1]"))); + EXPECT_TRUE(matcher.Matches( + CreateOriginFromString("https://[3ffe:2a00:100:7031::1]:443"))); + + EXPECT_FALSE(matcher.Matches( + CreateOriginFromString("http://[3ffe:2a00:100:7031::1]"))); + EXPECT_FALSE(matcher.Matches( + CreateOriginFromString("http://[3ffe:2a00:100:7031::1]:443"))); + EXPECT_FALSE(matcher.Matches( + CreateOriginFromString("https://[3ffe:2a00:100:7031::1]:8080"))); +} + +TEST_F(OriginMatcherTest, WildcardMatchesEveryOrigin) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("*")); + + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://foo.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://www.example.com"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://www.example.com:8080"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("http://192.168.0.1"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("http://192.168.0.1:8080"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("https://[a:b:c:d::]"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("https://[a:b:c:d::]:8080"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("ftp://example.com"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("about:blank"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString( + "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("file:///usr/local/a.txt"))); + EXPECT_TRUE(matcher.Matches(CreateOriginFromString( + "blob:http://127.0.0.1:8080/0530b9d1-c1c2-40ff-9f9c-c57336646baa"))); +} + +TEST_F(OriginMatcherTest, FileOrigin) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("file://")); + + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("file:///sdcard"))); + EXPECT_TRUE( + matcher.Matches(CreateOriginFromString("file:///android_assets"))); +} + +TEST_F(OriginMatcherTest, CustomSchemeOrigin) { + OriginMatcher matcher; + EXPECT_TRUE(matcher.AddRuleFromString("x-mail://")); + + EXPECT_TRUE(matcher.Matches(CreateOriginFromString("x-mail://hostname"))); +} + +namespace { + +void CompareMatcherRules(const OriginMatcherRule& r1, + const OriginMatcherRule& r2) { + ASSERT_EQ(r1.type(), r2.type()); + if (r1.type() == js_injection::OriginMatcherRuleType::kAny) + return; + const SubdomainMatchingRule& s1 = + static_cast<const SubdomainMatchingRule&>(r1); + const SubdomainMatchingRule& s2 = + static_cast<const SubdomainMatchingRule&>(r2); + EXPECT_EQ(s1.scheme(), s2.scheme()); + EXPECT_EQ(s1.optional_host(), s2.optional_host()); + EXPECT_EQ(s1.optional_port(), s2.optional_port()); +} + +void CompareMatchers(const OriginMatcher& m1, const OriginMatcher& m2) { + ASSERT_EQ(m1.rules().size(), m2.rules().size()); + for (size_t i = 0; i < m1.rules().size(); ++i) { + ASSERT_NO_FATAL_FAILURE( + CompareMatcherRules(*(m1.rules()[i].get()), *(m2.rules())[i].get())); + } +} + +} // namespace + +TEST_F(OriginMatcherTest, SerializeAndDeserializeMatchAll) { + OriginMatcher matcher; + OriginMatcher deserialized; + ASSERT_TRUE(matcher.AddRuleFromString("*")); + ASSERT_TRUE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); + ASSERT_NO_FATAL_FAILURE(CompareMatchers(matcher, deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeSubdomainMatcher) { + OriginMatcher matcher; + OriginMatcher deserialized; + ASSERT_TRUE(matcher.AddRuleFromString("https://*.example.com")); + ASSERT_TRUE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); + ASSERT_NO_FATAL_FAILURE(CompareMatchers(matcher, deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeInvalidSubdomain) { + OriginMatcher matcher; + OriginMatcher deserialized; + { + OriginMatcher::RuleList rules; + // The subdomain is not allowed to have a '/'. + rules.push_back(std::make_unique<SubdomainMatchingRule>( + "http", "bogus/host", 100, true)); + matcher.SetRules(std::move(rules)); + } + EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeInvalidScheme) { + OriginMatcher matcher; + OriginMatcher deserialized; + { + OriginMatcher::RuleList rules; + // The scheme can not be empty. + rules.push_back(std::make_unique<SubdomainMatchingRule>(std::string(), + "host", 101, true)); + matcher.SetRules(std::move(rules)); + } + EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeTooManyWildcards) { + OriginMatcher matcher; + OriginMatcher deserialized; + { + OriginMatcher::RuleList rules; + // Only one wildcard is allowed. + rules.push_back( + std::make_unique<SubdomainMatchingRule>("http", "**", 101, true)); + matcher.SetRules(std::move(rules)); + } + EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeInvalidWildcard) { + OriginMatcher matcher; + OriginMatcher deserialized; + { + OriginMatcher::RuleList rules; + // The wild card must be at the front. + rules.push_back( + std::make_unique<SubdomainMatchingRule>("http", "ab*", 101, true)); + matcher.SetRules(std::move(rules)); + } + EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); +} + +TEST_F(OriginMatcherTest, SerializeAndDeserializeValidWildcard) { + OriginMatcher matcher; + OriginMatcher deserialized; + { + OriginMatcher::RuleList rules; + // The wild card must be at the front. + rules.push_back( + std::make_unique<SubdomainMatchingRule>("http", "*.ab", 101, true)); + matcher.SetRules(std::move(rules)); + } + EXPECT_TRUE(mojo::test::SerializeAndDeserialize<mojom::OriginMatcher>( + &matcher, &deserialized)); + ASSERT_NO_FATAL_FAILURE(CompareMatchers(matcher, deserialized)); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/renderer/BUILD.gn b/chromium/components/js_injection/renderer/BUILD.gn new file mode 100644 index 00000000000..3cdd5294923 --- /dev/null +++ b/chromium/components/js_injection/renderer/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright 2020 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. + +source_set("renderer") { + sources = [ + "js_binding.cc", + "js_binding.h", + "js_communication.cc", + "js_communication.h", + ] + + deps = [ + "//base", + "//components/js_injection/common", + "//components/js_injection/common:common_mojom", + "//content/public/common", + "//content/public/renderer", + "//gin", + "//mojo/public/cpp/bindings", + "//third_party/blink/public:blink", + "//url", + "//v8", + ] +} diff --git a/chromium/components/js_injection/renderer/DEPS b/chromium/components/js_injection/renderer/DEPS new file mode 100644 index 00000000000..33ec23100fe --- /dev/null +++ b/chromium/components/js_injection/renderer/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+gin", + "+v8/include/v8.h", +] diff --git a/chromium/components/js_injection/renderer/js_binding.cc b/chromium/components/js_injection/renderer/js_binding.cc new file mode 100644 index 00000000000..f4c5798990c --- /dev/null +++ b/chromium/components/js_injection/renderer/js_binding.cc @@ -0,0 +1,246 @@ +// Copyright 2019 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 "components/js_injection/renderer/js_binding.h" + +#include <vector> + +#include "base/strings/string_util.h" +#include "components/js_injection/renderer/js_communication.h" +#include "content/public/renderer/render_frame.h" +#include "gin/data_object_builder.h" +#include "gin/handle.h" +#include "gin/object_template_builder.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" +#include "third_party/blink/public/common/messaging/message_port_channel.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/web/blink.h" +#include "third_party/blink/public/web/web_frame.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_message_port_converter.h" +#include "v8/include/v8.h" + +namespace { +constexpr char kPostMessage[] = "postMessage"; +constexpr char kOnMessage[] = "onmessage"; +constexpr char kAddEventListener[] = "addEventListener"; +constexpr char kRemoveEventListener[] = "removeEventListener"; +} // anonymous namespace + +namespace js_injection { + +gin::WrapperInfo JsBinding::kWrapperInfo = {gin::kEmbedderNativeGin}; + +// static +std::unique_ptr<JsBinding> JsBinding::Install( + content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator) { + CHECK(!js_object_name.empty()) + << "JavaScript wrapper name shouldn't be empty"; + + v8::Isolate* isolate = blink::MainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Context> context = + render_frame->GetWebFrame()->MainWorldScriptContext(); + if (context.IsEmpty()) + return nullptr; + + v8::Context::Scope context_scope(context); + std::unique_ptr<JsBinding> js_binding( + new JsBinding(render_frame, js_object_name, js_java_configurator)); + gin::Handle<JsBinding> bindings = + gin::CreateHandle(isolate, js_binding.get()); + if (bindings.IsEmpty()) + return nullptr; + + v8::Local<v8::Object> global = context->Global(); + global + ->CreateDataProperty(context, + gin::StringToSymbol(isolate, js_object_name), + bindings.ToV8()) + .Check(); + + return js_binding; +} + +JsBinding::JsBinding(content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator) + : render_frame_(render_frame), + js_object_name_(js_object_name), + js_java_configurator_(js_java_configurator) { + mojom::JsToBrowserMessaging* js_to_java_messaging = + js_java_configurator_->GetJsToJavaMessage(js_object_name_); + if (js_to_java_messaging) { + js_to_java_messaging->SetBrowserToJsMessaging( + receiver_.BindNewEndpointAndPassRemote()); + } +} + +JsBinding::~JsBinding() = default; + +void JsBinding::OnPostMessage(const base::string16& message) { + v8::Isolate* isolate = blink::MainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + if (!web_frame) + return; + + v8::Local<v8::Context> context = web_frame->MainWorldScriptContext(); + if (context.IsEmpty()) + return; + + v8::Context::Scope context_scope(context); + // Setting verbose makes the exception get reported to the default + // uncaught-exception handlers, rather than just being silently swallowed. + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(true); + + // Simulate MessageEvent's data property. See + // https://html.spec.whatwg.org/multipage/comms.html#messageevent + v8::Local<v8::Object> event = + gin::DataObjectBuilder(isolate).Set("data", message).Build(); + v8::Local<v8::Value> argv[] = {event}; + + v8::Local<v8::Object> self = GetWrapper(isolate).ToLocalChecked(); + v8::Local<v8::Function> on_message = GetOnMessage(isolate); + if (!on_message.IsEmpty()) { + web_frame->RequestExecuteV8Function(context, on_message, self, 1, argv, + nullptr); + } + + for (const auto& listener : listeners_) { + web_frame->RequestExecuteV8Function(context, listener.Get(isolate), self, 1, + argv, nullptr); + } +} + +void JsBinding::ReleaseV8GlobalObjects() { + listeners_.clear(); + on_message_.Reset(); +} + +gin::ObjectTemplateBuilder JsBinding::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin::Wrappable<JsBinding>::GetObjectTemplateBuilder(isolate) + .SetMethod(kPostMessage, &JsBinding::PostMessage) + .SetMethod(kAddEventListener, &JsBinding::AddEventListener) + .SetMethod(kRemoveEventListener, &JsBinding::RemoveEventListener) + .SetProperty(kOnMessage, &JsBinding::GetOnMessage, + &JsBinding::SetOnMessage); +} + +void JsBinding::PostMessage(gin::Arguments* args) { + base::string16 message; + if (!args->GetNext(&message)) { + args->ThrowError(); + return; + } + + std::vector<blink::MessagePortChannel> ports; + std::vector<v8::Local<v8::Object>> objs; + // If we get more than two arguments and the second argument is not an array + // of ports, we can't process. + if (args->Length() >= 2 && !args->GetNext(&objs)) { + args->ThrowError(); + return; + } + + for (auto& obj : objs) { + base::Optional<blink::MessagePortChannel> port = + blink::WebMessagePortConverter::DisentangleAndExtractMessagePortChannel( + args->isolate(), obj); + // If the port is null we should throw an exception. + if (!port.has_value()) { + args->ThrowError(); + return; + } + ports.emplace_back(port.value()); + } + + mojom::JsToBrowserMessaging* js_to_java_messaging = + js_java_configurator_->GetJsToJavaMessage(js_object_name_); + if (js_to_java_messaging) { + js_to_java_messaging->PostMessage( + message, blink::MessagePortChannel::ReleaseHandles(ports)); + } +} + +// AddEventListener() needs to match EventTarget's AddEventListener() in blink. +// It takes |type|, |listener| parameters, we ignore the |options| parameter. +// See https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener +void JsBinding::AddEventListener(gin::Arguments* args) { + std::string type; + if (!args->GetNext(&type)) { + args->ThrowError(); + return; + } + + // We only support message event. + if (type != "message") + return; + + v8::Local<v8::Function> listener; + if (!args->GetNext(&listener)) + return; + + // Should be at most 3 parameters. + if (args->Length() > 3) { + args->ThrowError(); + return; + } + + if (base::Contains(listeners_, listener)) + return; + + v8::Local<v8::Context> context = args->GetHolderCreationContext(); + listeners_.push_back( + v8::Global<v8::Function>(context->GetIsolate(), listener)); +} + +// RemoveEventListener() needs to match EventTarget's RemoveEventListener() in +// blink. It takes |type|, |listener| parameters, we ignore |options| parameter. +// See https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener +void JsBinding::RemoveEventListener(gin::Arguments* args) { + std::string type; + if (!args->GetNext(&type)) { + args->ThrowError(); + return; + } + + // We only support message event. + if (type != "message") + return; + + v8::Local<v8::Function> listener; + if (!args->GetNext(&listener)) + return; + + // Should be at most 3 parameters. + if (args->Length() > 3) { + args->ThrowError(); + return; + } + + auto iter = std::find(listeners_.begin(), listeners_.end(), listener); + if (iter == listeners_.end()) + return; + + listeners_.erase(iter); +} + +v8::Local<v8::Function> JsBinding::GetOnMessage(v8::Isolate* isolate) { + return on_message_.Get(isolate); +} + +void JsBinding::SetOnMessage(v8::Isolate* isolate, v8::Local<v8::Value> value) { + if (value->IsFunction()) + on_message_.Reset(isolate, value.As<v8::Function>()); + else + on_message_.Reset(); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/renderer/js_binding.h b/chromium/components/js_injection/renderer/js_binding.h new file mode 100644 index 00000000000..1fca33f9110 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_binding.h @@ -0,0 +1,87 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ +#define COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ + +#include <string> + +#include "base/auto_reset.h" +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "gin/arguments.h" +#include "gin/wrappable.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/associated_remote.h" + +namespace v8 { +template <typename T> +class Global; +class Function; +} // namespace v8 + +namespace content { +class RenderFrame; +} // namespace content + +namespace js_injection { +class JsCommunication; +// A gin::Wrappable class used for providing JavaScript API. We will inject the +// object of this class to JavaScript world in JsCommunication. +// JsCommunication will own at most one instance of this class. When the +// RenderFrame gone or another DidClearWindowObject comes, the instance will be +// destroyed. +class JsBinding : public gin::Wrappable<JsBinding>, + public mojom::BrowserToJsMessaging { + public: + static gin::WrapperInfo kWrapperInfo; + + static std::unique_ptr<JsBinding> Install( + content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator); + + // mojom::BrowserToJsMessaging implementation. + void OnPostMessage(const base::string16& message) override; + + void ReleaseV8GlobalObjects(); + + ~JsBinding() final; + + private: + explicit JsBinding(content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator); + + // gin::Wrappable implementation + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) final; + + // For jsObject.postMessage(message[, ports]) JavaScript API. + void PostMessage(gin::Arguments* args); + // For jsObject.addEventListener("message", listener) JavaScript API. + void AddEventListener(gin::Arguments* args); + // For jsObject.removeEventListener("message", listener) JavaScript API. + void RemoveEventListener(gin::Arguments* args); + // For get jsObject.onmessage. + v8::Local<v8::Function> GetOnMessage(v8::Isolate* isolate); + // For set jsObject.onmessage. + void SetOnMessage(v8::Isolate* isolate, v8::Local<v8::Value> value); + + content::RenderFrame* render_frame_; + base::string16 js_object_name_; + v8::Global<v8::Function> on_message_; + std::vector<v8::Global<v8::Function>> listeners_; + // |js_java_configurator| owns JsBinding objects, so it will out live + // JsBinding's life cycle, it is safe to access it. + JsCommunication* js_java_configurator_; + + mojo::AssociatedReceiver<mojom::BrowserToJsMessaging> receiver_{this}; + + DISALLOW_COPY_AND_ASSIGN(JsBinding); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ diff --git a/chromium/components/js_injection/renderer/js_communication.cc b/chromium/components/js_injection/renderer/js_communication.cc new file mode 100644 index 00000000000..dd648297f20 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_communication.cc @@ -0,0 +1,133 @@ +// Copyright 2019 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 "components/js_injection/renderer/js_communication.h" + +#include "components/js_injection/common/origin_matcher.h" +#include "components/js_injection/renderer/js_binding.h" +#include "content/public/common/isolated_world_ids.h" +#include "content/public/renderer/render_frame.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" +#include "third_party/blink/public/platform/task_type.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_script_source.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace js_injection { + +struct JsCommunication::JsObjectInfo { + OriginMatcher origin_matcher; + mojo::AssociatedRemote<mojom::JsToBrowserMessaging> js_to_java_messaging; +}; + +struct JsCommunication::DocumentStartJavaScript { + OriginMatcher origin_matcher; + blink::WebString script; + int32_t script_id; +}; + +JsCommunication::JsCommunication(content::RenderFrame* render_frame) + : RenderFrameObserver(render_frame), + RenderFrameObserverTracker<JsCommunication>(render_frame) { + render_frame->GetAssociatedInterfaceRegistry()->AddInterface( + base::BindRepeating(&JsCommunication::BindPendingReceiver, + base::Unretained(this))); +} + +JsCommunication::~JsCommunication() = default; + +void JsCommunication::SetJsObjects( + std::vector<mojom::JsObjectPtr> js_object_ptrs) { + JsObjectMap js_objects; + for (const auto& js_object : js_object_ptrs) { + const auto& js_object_info_pair = js_objects.insert( + {js_object->js_object_name, std::make_unique<JsObjectInfo>()}); + JsObjectInfo* js_object_info = js_object_info_pair.first->second.get(); + js_object_info->origin_matcher = js_object->origin_matcher; + js_object_info->js_to_java_messaging = + mojo::AssociatedRemote<mojom::JsToBrowserMessaging>( + std::move(js_object->js_to_browser_messaging)); + } + js_objects_.swap(js_objects); +} + +void JsCommunication::AddDocumentStartScript( + mojom::DocumentStartJavaScriptPtr script_ptr) { + DocumentStartJavaScript* script = new DocumentStartJavaScript{ + script_ptr->origin_matcher, + blink::WebString::FromUTF16(script_ptr->script), script_ptr->script_id}; + scripts_.push_back(std::unique_ptr<DocumentStartJavaScript>(script)); +} + +void JsCommunication::RemoveDocumentStartScript(int32_t script_id) { + for (auto it = scripts_.begin(); it != scripts_.end(); ++it) { + if ((*it)->script_id == script_id) { + scripts_.erase(it); + break; + } + } +} + +void JsCommunication::DidClearWindowObject() { + if (inside_did_clear_window_object_) + return; + + base::AutoReset<bool> flag_entry(&inside_did_clear_window_object_, true); + + url::Origin frame_origin = + url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin()); + std::vector<std::unique_ptr<JsBinding>> js_bindings; + js_bindings.reserve(js_objects_.size()); + for (const auto& js_object : js_objects_) { + if (!js_object.second->origin_matcher.Matches(frame_origin)) + continue; + js_bindings.push_back( + JsBinding::Install(render_frame(), js_object.first, this)); + } + js_bindings_.swap(js_bindings); +} + +void JsCommunication::WillReleaseScriptContext(v8::Local<v8::Context> context, + int32_t world_id) { + // We created v8 global objects only in the main world, should clear them only + // when this is for main world. + if (world_id != content::ISOLATED_WORLD_ID_GLOBAL) + return; + + for (const auto& js_binding : js_bindings_) + js_binding->ReleaseV8GlobalObjects(); +} + +void JsCommunication::OnDestruct() { + delete this; +} + +void JsCommunication::RunScriptsAtDocumentStart() { + url::Origin frame_origin = + url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin()); + for (const auto& script : scripts_) { + if (!script->origin_matcher.Matches(frame_origin)) + continue; + render_frame()->GetWebFrame()->ExecuteScript( + blink::WebScriptSource(script->script)); + } +} + +void JsCommunication::BindPendingReceiver( + mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver) { + receiver_.Bind(std::move(pending_receiver), + render_frame()->GetTaskRunner( + blink::TaskType::kInternalNavigationAssociated)); +} + +mojom::JsToBrowserMessaging* JsCommunication::GetJsToJavaMessage( + const base::string16& js_object_name) { + auto iterator = js_objects_.find(js_object_name); + if (iterator == js_objects_.end()) + return nullptr; + return iterator->second->js_to_java_messaging.get(); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/renderer/js_communication.h b/chromium/components/js_injection/renderer/js_communication.h new file mode 100644 index 00000000000..8986e784c65 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_communication.h @@ -0,0 +1,76 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ +#define COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_frame_observer_tracker.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/associated_remote.h" +#include "third_party/blink/public/platform/web_string.h" + +namespace content { +class RenderFrame; +} + +namespace js_injection { + +class JsBinding; + +class JsCommunication + : public mojom::JsCommunication, + public content::RenderFrameObserver, + public content::RenderFrameObserverTracker<JsCommunication> { + public: + explicit JsCommunication(content::RenderFrame* render_frame); + ~JsCommunication() override; + + // mojom::JsCommunication implementation + void SetJsObjects(std::vector<mojom::JsObjectPtr> js_object_ptrs) override; + void AddDocumentStartScript( + mojom::DocumentStartJavaScriptPtr script_ptr) override; + void RemoveDocumentStartScript(int32_t script_id) override; + + // RenderFrameObserver implementation + void DidClearWindowObject() override; + void WillReleaseScriptContext(v8::Local<v8::Context> context, + int32_t world_id) override; + void OnDestruct() override; + + void RunScriptsAtDocumentStart(); + + mojom::JsToBrowserMessaging* GetJsToJavaMessage( + const base::string16& js_object_name); + + private: + struct JsObjectInfo; + struct DocumentStartJavaScript; + + void BindPendingReceiver( + mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver); + + using JsObjectMap = std::map<base::string16, std::unique_ptr<JsObjectInfo>>; + JsObjectMap js_objects_; + + // In some cases DidClearWindowObject will be called twice in a row, we need + // to prevent doing multiple injection in that case. + bool inside_did_clear_window_object_ = false; + + std::vector<std::unique_ptr<DocumentStartJavaScript>> scripts_; + std::vector<std::unique_ptr<JsBinding>> js_bindings_; + + // Associated with legacy IPC channel. + mojo::AssociatedReceiver<mojom::JsCommunication> receiver_{this}; + + DISALLOW_COPY_AND_ASSIGN(JsCommunication); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ |