diff options
Diffstat (limited to 'chromium/components/js_injection/common')
11 files changed, 1110 insertions, 0 deletions
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 |