| // 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 URL_ORIGIN_ABSTRACT_TESTS_H_ |
| #define URL_ORIGIN_ABSTRACT_TESTS_H_ |
| |
| #include <string> |
| #include <type_traits> |
| |
| #include "base/containers/contains.h" |
| #include "base/strings/string_piece.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/scheme_host_port.h" |
| #include "url/url_util.h" |
| |
| namespace url { |
| |
| void ExpectParsedUrlsEqual(const GURL& a, const GURL& b); |
| |
| // AbstractOriginTest below abstracts away differences between url::Origin and |
| // blink::SecurityOrigin by parametrizing the tests with a class that has to |
| // expose the same public members as UrlOriginTestTraits below. |
| class UrlOriginTestTraits { |
| public: |
| using OriginType = Origin; |
| |
| // Constructing an origin. |
| static OriginType CreateOriginFromString(base::StringPiece s); |
| static OriginType CreateUniqueOpaqueOrigin(); |
| static OriginType CreateWithReferenceOrigin( |
| base::StringPiece url, |
| const OriginType& reference_origin); |
| static OriginType DeriveNewOpaqueOrigin(const OriginType& reference_origin); |
| |
| // Accessors for origin properties. |
| static bool IsOpaque(const OriginType& origin); |
| static std::string GetScheme(const OriginType& origin); |
| static std::string GetHost(const OriginType& origin); |
| static uint16_t GetPort(const OriginType& origin); |
| static SchemeHostPort GetTupleOrPrecursorTupleIfOpaque( |
| const OriginType& origin); |
| |
| // Wrappers for other instance methods of OriginType. |
| static bool IsSameOrigin(const OriginType& a, const OriginType& b); |
| static std::string Serialize(const OriginType& origin); |
| |
| // "Accessors" of URL properties. |
| // |
| // TODO(lukasza): Consider merging together OriginTraitsBase here and |
| // UrlTraitsBase in //url/gurl_abstract_tests.h. |
| static bool IsValidUrl(base::StringPiece str); |
| |
| // Only static members = no constructors are needed. |
| UrlOriginTestTraits() = delete; |
| }; |
| |
| // Test suite for tests that cover both url::Origin and blink::SecurityOrigin. |
| template <typename TOriginTraits> |
| class AbstractOriginTest : public testing::Test { |
| public: |
| void SetUp() override { |
| const char* kSchemesToRegister[] = { |
| "noaccess", |
| "std-with-host", |
| "noaccess-std-with-host", |
| "local", |
| "local-noaccess", |
| "local-std-with-host", |
| "local-noaccess-std-with-host", |
| "also-local", |
| "sec", |
| "sec-std-with-host", |
| "sec-noaccess", |
| }; |
| for (const char* kScheme : kSchemesToRegister) { |
| std::string scheme(kScheme); |
| if (base::Contains(scheme, "noaccess")) |
| AddNoAccessScheme(kScheme); |
| if (base::Contains(scheme, "std-with-host")) |
| AddStandardScheme(kScheme, SchemeType::SCHEME_WITH_HOST); |
| if (base::Contains(scheme, "local")) |
| AddLocalScheme(kScheme); |
| if (base::Contains(scheme, "sec")) |
| AddSecureScheme(kScheme); |
| } |
| } |
| |
| protected: |
| // Wrappers that help ellide away TOriginTraits. |
| // |
| // Note that calling the wrappers needs to be prefixed with `this->...` to |
| // avoid hitting: explicit qualification required to use member 'IsOpaque' |
| // from dependent base class. |
| using OriginType = typename TOriginTraits::OriginType; |
| OriginType CreateOriginFromString(base::StringPiece s) { |
| return TOriginTraits::CreateOriginFromString(s); |
| } |
| OriginType CreateUniqueOpaqueOrigin() { |
| return TOriginTraits::CreateUniqueOpaqueOrigin(); |
| } |
| OriginType CreateWithReferenceOrigin(base::StringPiece url, |
| const OriginType& reference_origin) { |
| return TOriginTraits::CreateWithReferenceOrigin(url, reference_origin); |
| } |
| OriginType DeriveNewOpaqueOrigin(const OriginType& reference_origin) { |
| return TOriginTraits::DeriveNewOpaqueOrigin(reference_origin); |
| } |
| bool IsOpaque(const OriginType& origin) { |
| return TOriginTraits::IsOpaque(origin); |
| } |
| std::string GetScheme(const OriginType& origin) { |
| return TOriginTraits::GetScheme(origin); |
| } |
| std::string GetHost(const OriginType& origin) { |
| return TOriginTraits::GetHost(origin); |
| } |
| uint16_t GetPort(const OriginType& origin) { |
| return TOriginTraits::GetPort(origin); |
| } |
| SchemeHostPort GetTupleOrPrecursorTupleIfOpaque(const OriginType& origin) { |
| return TOriginTraits::GetTupleOrPrecursorTupleIfOpaque(origin); |
| } |
| bool IsSameOrigin(const OriginType& a, const OriginType& b) { |
| bool is_a_same_with_b = TOriginTraits::IsSameOrigin(a, b); |
| bool is_b_same_with_a = TOriginTraits::IsSameOrigin(b, a); |
| EXPECT_EQ(is_a_same_with_b, is_b_same_with_a); |
| return is_a_same_with_b; |
| } |
| std::string Serialize(const OriginType& origin) { |
| return TOriginTraits::Serialize(origin); |
| } |
| bool IsValidUrl(base::StringPiece str) { |
| return TOriginTraits::IsValidUrl(str); |
| } |
| |
| #define EXPECT_SAME_ORIGIN(a, b) \ |
| EXPECT_TRUE(this->IsSameOrigin((a), (b))) \ |
| << "When checking if \"" << this->Serialize(a) << "\" is " \ |
| << "same-origin with \"" << this->Serialize(b) << "\"" |
| |
| #define EXPECT_CROSS_ORIGIN(a, b) \ |
| EXPECT_FALSE(this->IsSameOrigin((a), (b))) \ |
| << "When checking if \"" << this->Serialize(a) << "\" is " \ |
| << "cross-origin from \"" << this->Serialize(b) << "\"" |
| |
| void VerifyOriginInvariants(const OriginType& origin) { |
| // An origin is always same-origin with itself. |
| EXPECT_SAME_ORIGIN(origin, origin); |
| |
| // A copy of |origin| should be same-origin as well. |
| auto origin_copy = origin; |
| EXPECT_EQ(this->GetScheme(origin), this->GetScheme(origin_copy)); |
| EXPECT_EQ(this->GetHost(origin), this->GetHost(origin_copy)); |
| EXPECT_EQ(this->GetPort(origin), this->GetPort(origin_copy)); |
| EXPECT_EQ(this->IsOpaque(origin), this->IsOpaque(origin_copy)); |
| EXPECT_SAME_ORIGIN(origin, origin_copy); |
| |
| // An origin is always cross-origin from another, unique, opaque origin. |
| EXPECT_CROSS_ORIGIN(origin, this->CreateUniqueOpaqueOrigin()); |
| |
| // An origin is always cross-origin from another tuple origin. |
| auto different_tuple_origin = |
| this->CreateOriginFromString("https://not-in-the-list.test/"); |
| EXPECT_CROSS_ORIGIN(origin, different_tuple_origin); |
| |
| // Deriving an origin for "about:blank". |
| auto about_blank_origin1 = |
| this->CreateWithReferenceOrigin("about:blank", origin); |
| auto about_blank_origin2 = |
| this->CreateWithReferenceOrigin("about:blank?bar#foo", origin); |
| EXPECT_SAME_ORIGIN(origin, about_blank_origin1); |
| EXPECT_SAME_ORIGIN(origin, about_blank_origin2); |
| |
| // Derived opaque origins. |
| std::vector<OriginType> derived_origins = { |
| this->DeriveNewOpaqueOrigin(origin), |
| this->CreateWithReferenceOrigin("data:text/html,baz", origin), |
| this->DeriveNewOpaqueOrigin(about_blank_origin1), |
| }; |
| for (size_t i = 0; i < derived_origins.size(); i++) { |
| SCOPED_TRACE(testing::Message() << "Derived origin #" << i); |
| const OriginType& derived_origin = derived_origins[i]; |
| EXPECT_TRUE(this->IsOpaque(derived_origin)); |
| EXPECT_SAME_ORIGIN(derived_origin, derived_origin); |
| EXPECT_CROSS_ORIGIN(origin, derived_origin); |
| EXPECT_EQ(this->GetTupleOrPrecursorTupleIfOpaque(origin), |
| this->GetTupleOrPrecursorTupleIfOpaque(derived_origin)); |
| } |
| } |
| |
| void VerifyUniqueOpaqueOriginInvariants(const OriginType& origin) { |
| if (!this->IsOpaque(origin)) { |
| ADD_FAILURE() << "Got unexpectedly non-opaque origin: " |
| << this->Serialize(origin); |
| return; // Skip other test assertions. |
| } |
| |
| // Opaque origins should have an "empty" scheme, host and port. |
| EXPECT_EQ("", this->GetScheme(origin)); |
| EXPECT_EQ("", this->GetHost(origin)); |
| EXPECT_EQ(0, this->GetPort(origin)); |
| |
| // Unique opaque origins should have an empty precursor tuple. |
| EXPECT_EQ(SchemeHostPort(), this->GetTupleOrPrecursorTupleIfOpaque(origin)); |
| |
| // Serialization test. |
| EXPECT_EQ("null", this->Serialize(origin)); |
| |
| // Invariants that should hold for any origin. |
| VerifyOriginInvariants(origin); |
| } |
| |
| void TestUniqueOpaqueOrigin(base::StringPiece test_input) { |
| auto origin = this->CreateOriginFromString(test_input); |
| this->VerifyUniqueOpaqueOriginInvariants(origin); |
| |
| // Re-creating from the URL should be cross-origin. |
| auto origin_recreated_from_same_input = |
| this->CreateOriginFromString(test_input); |
| EXPECT_CROSS_ORIGIN(origin, origin_recreated_from_same_input); |
| } |
| |
| void VerifyTupleOriginInvariants(const OriginType& origin, |
| const SchemeHostPort& expected_tuple) { |
| if (this->IsOpaque(origin)) { |
| ADD_FAILURE() << "Got unexpectedly opaque origin"; |
| return; // Skip other test assertions. |
| } |
| SCOPED_TRACE(testing::Message() |
| << "Actual origin: " << this->Serialize(origin)); |
| |
| // Compare `origin` against the `expected_tuple`. |
| EXPECT_EQ(expected_tuple.scheme(), this->GetScheme(origin)); |
| EXPECT_EQ(expected_tuple.host(), this->GetHost(origin)); |
| EXPECT_EQ(expected_tuple.port(), this->GetPort(origin)); |
| EXPECT_EQ(expected_tuple, this->GetTupleOrPrecursorTupleIfOpaque(origin)); |
| |
| // Serialization test. |
| // |
| // TODO(lukasza): Consider preserving the hostname when serializing file: |
| // URLs. Dropping the hostname seems incompatible with section 6 of |
| // rfc6454. Even though section 4 says that "the implementation MAY |
| // return an implementation-defined value", it seems that Chromium |
| // implementation *does* include the hostname in the origin SchemeHostPort |
| // tuple. |
| if (expected_tuple.scheme() != kFileScheme || expected_tuple.host() == "") { |
| EXPECT_SAME_ORIGIN(origin, |
| this->CreateOriginFromString(this->Serialize(origin))); |
| } |
| |
| // Invariants that should hold for any origin. |
| VerifyOriginInvariants(origin); |
| } |
| |
| private: |
| ScopedSchemeRegistryForTests scoped_scheme_registry_; |
| }; |
| |
| TYPED_TEST_SUITE_P(AbstractOriginTest); |
| |
| TYPED_TEST_P(AbstractOriginTest, NonStandardSchemeWithAndroidWebViewHack) { |
| EnableNonStandardSchemesForAndroidWebView(); |
| |
| // Regression test for https://crbug.com/896059. |
| auto origin = this->CreateOriginFromString("unknown-scheme://"); |
| EXPECT_FALSE(this->IsOpaque(origin)); |
| EXPECT_EQ("unknown-scheme", this->GetScheme(origin)); |
| EXPECT_EQ("", this->GetHost(origin)); |
| EXPECT_EQ(0, this->GetPort(origin)); |
| |
| // about:blank translates into an opaque origin, even in presence of |
| // EnableNonStandardSchemesForAndroidWebView. |
| origin = this->CreateOriginFromString("about:blank"); |
| EXPECT_TRUE(this->IsOpaque(origin)); |
| } |
| |
| TYPED_TEST_P(AbstractOriginTest, OpaqueOriginsFromValidUrls) { |
| const char* kTestCases[] = { |
| // Built-in noaccess schemes. |
| "data:text/html,Hello!", |
| "javascript:alert(1)", |
| "about:blank", |
| |
| // Opaque blob URLs. |
| "blob:null/foo", // blob:null (actually a valid URL) |
| "blob:data:foo", // blob + data (which is nonstandard) |
| "blob:about://blank/", // blob + about (which is nonstandard) |
| "blob:about:blank/", // blob + about (which is nonstandard) |
| "blob:blob:http://www.example.com/guid-goes-here", |
| "blob:filesystem:ws:b/.", |
| "blob:filesystem:ftp://a/b", |
| "blob:blob:file://localhost/foo/bar", |
| }; |
| |
| for (const char* test_input : kTestCases) { |
| SCOPED_TRACE(testing::Message() << "Test input: " << test_input); |
| |
| // Verify that `origin` is opaque not just because `test_input` results is |
| // an invalid URL (because of a typo in the scheme name, or because of a |
| // technicality like having no host in a noaccess-std-with-host: scheme). |
| EXPECT_TRUE(this->IsValidUrl(test_input)); |
| |
| this->TestUniqueOpaqueOrigin(test_input); |
| } |
| } |
| |
| TYPED_TEST_P(AbstractOriginTest, OpaqueOriginsFromInvalidUrls) { |
| // TODO(lukasza): Consider moving those to GURL/KURL tests that verify what |
| // inputs are parsed as an invalid URL. |
| |
| const char* kTestCases[] = { |
| // Invalid file: URLs. |
| "file://example.com:443/etc/passwd", // No port expected. |
| |
| // Invalid HTTP URLs. |
| "http", |
| "http:", |
| "http:/", |
| "http://", |
| "http://:", |
| "http://:1", |
| "http::///invalid.example.com/", |
| "http://example.com:65536/", // Port out of range. |
| "http://example.com:-1/", // Port out of range. |
| "http://example.com:18446744073709551616/", // Port = 2^64. |
| "http://example.com:18446744073709551616999/", // Lots of port digits. |
| |
| // Invalid filesystem URLs. |
| "filesystem:http://example.com/", // Missing /type/. |
| "filesystem:local:baz./type/", |
| "filesystem:local://hostname/type/", |
| "filesystem:unknown-scheme://hostname/type/", |
| "filesystem:filesystem:http://example.org:88/foo/bar", |
| |
| // Invalid IP addresses |
| "http://[]/", |
| "http://[2001:0db8:0000:0000:0000:0000:0000:0000:0001]/", // 9 groups. |
| |
| // Unknown scheme without a colon character (":") gives an invalid URL. |
| "unknown-scheme", |
| |
| // Standard schemes require a hostname (and result in an opaque origin if |
| // the hostname is missing). |
| "local-std-with-host:", |
| "noaccess-std-with-host:", |
| }; |
| |
| for (const char* test_input : kTestCases) { |
| SCOPED_TRACE(testing::Message() << "Test input: " << test_input); |
| |
| // All testcases here are expected to represent invalid URLs. |
| // an invalid URL (because of a type in scheme name, or because of a |
| // technicality like having no host in a noaccess-std-with-host: scheme). |
| EXPECT_FALSE(this->IsValidUrl(test_input)); |
| |
| // Invalid URLs should always result in an opaque origin. |
| this->TestUniqueOpaqueOrigin(test_input); |
| } |
| } |
| |
| TYPED_TEST_P(AbstractOriginTest, TupleOrigins) { |
| struct TestCase { |
| const char* input; |
| SchemeHostPort expected_tuple; |
| } kTestCases[] = { |
| // file: URLs |
| {"file:///etc/passwd", {"file", "", 0}}, |
| {"file://example.com/etc/passwd", {"file", "example.com", 0}}, |
| {"file:///", {"file", "", 0}}, |
| {"file://hostname/C:/dir/file.txt", {"file", "hostname", 0}}, |
| |
| // HTTP URLs |
| {"http://example.com/", {"http", "example.com", 80}}, |
| {"http://example.com:80/", {"http", "example.com", 80}}, |
| {"http://example.com:123/", {"http", "example.com", 123}}, |
| {"http://example.com:0/", {"http", "example.com", 0}}, |
| {"http://example.com:65535/", {"http", "example.com", 65535}}, |
| {"https://example.com/", {"https", "example.com", 443}}, |
| {"https://example.com:443/", {"https", "example.com", 443}}, |
| {"https://example.com:123/", {"https", "example.com", 123}}, |
| {"https://example.com:0/", {"https", "example.com", 0}}, |
| {"https://example.com:65535/", {"https", "example.com", 65535}}, |
| {"http://user:pass@example.com/", {"http", "example.com", 80}}, |
| {"http://example.com:123/?query", {"http", "example.com", 123}}, |
| {"https://example.com/#1234", {"https", "example.com", 443}}, |
| {"https://u:p@example.com:123/?query#1234", |
| {"https", "example.com", 123}}, |
| {"http://example/", {"http", "example", 80}}, |
| |
| // Blob URLs. |
| {"blob:http://example.com/guid-goes-here", {"http", "example.com", 80}}, |
| {"blob:http://example.com:123/guid-goes-here", |
| {"http", "example.com", 123}}, |
| {"blob:https://example.com/guid-goes-here", |
| {"https", "example.com", 443}}, |
| {"blob:http://u:p@example.com/guid-goes-here", |
| {"http", "example.com", 80}}, |
| |
| // Filesystem URLs. |
| {"filesystem:http://example.com/type/", {"http", "example.com", 80}}, |
| {"filesystem:http://example.com:123/type/", {"http", "example.com", 123}}, |
| {"filesystem:https://example.com/type/", {"https", "example.com", 443}}, |
| {"filesystem:https://example.com:123/type/", |
| {"https", "example.com", 123}}, |
| {"filesystem:local-std-with-host:baz./type/", |
| {"local-std-with-host", "baz.", 0}}, |
| |
| // IP Addresses |
| {"http://192.168.9.1/", {"http", "192.168.9.1", 80}}, |
| {"http://[2001:db8::1]/", {"http", "[2001:db8::1]", 80}}, |
| {"http://[2001:0db8:0000:0000:0000:0000:0000:0001]/", |
| {"http", "[2001:db8::1]", 80}}, |
| {"http://1/", {"http", "0.0.0.1", 80}}, |
| {"http://1:1/", {"http", "0.0.0.1", 1}}, |
| {"http://3232237825/", {"http", "192.168.9.1", 80}}, |
| |
| // Punycode |
| {"http://☃.net/", {"http", "xn--n3h.net", 80}}, |
| {"blob:http://☃.net/", {"http", "xn--n3h.net", 80}}, |
| {"local-std-with-host:↑↑↓↓←→←→ba.↑↑↓↓←→←→ba.0.bg", |
| {"local-std-with-host", "xn--ba-rzuadaibfa.xn--ba-rzuadaibfa.0.bg", 0}}, |
| |
| // Registered URLs |
| {"ftp://example.com/", {"ftp", "example.com", 21}}, |
| {"ws://example.com/", {"ws", "example.com", 80}}, |
| {"wss://example.com/", {"wss", "example.com", 443}}, |
| {"wss://user:pass@example.com/", {"wss", "example.com", 443}}, |
| }; |
| |
| for (const TestCase& test : kTestCases) { |
| SCOPED_TRACE(testing::Message() << "Test input: " << test.input); |
| |
| // Only valid URLs should translate into valid, non-opaque origins. |
| EXPECT_TRUE(this->IsValidUrl(test.input)); |
| |
| auto origin = this->CreateOriginFromString(test.input); |
| this->VerifyTupleOriginInvariants(origin, test.expected_tuple); |
| } |
| } |
| |
| TYPED_TEST_P(AbstractOriginTest, CustomSchemes_OpaqueOrigins) { |
| const char* kTestCases[] = { |
| // Unknown scheme |
| "unknown-scheme:foo", |
| "unknown-scheme://bar", |
| |
| // Unknown scheme that is a prefix or suffix of a registered scheme. |
| "loca:foo", |
| "ocal:foo", |
| "local-suffix:foo", |
| "prefix-local:foo", |
| |
| // Custom no-access schemes translate into an opaque origin (just like the |
| // built-in no-access schemes such as about:blank or data:). |
| "noaccess-std-with-host:foo", |
| "noaccess-std-with-host://bar", |
| "noaccess://host", |
| "local-noaccess://host", |
| "local-noaccess-std-with-host://host", |
| }; |
| |
| for (const char* test_input : kTestCases) { |
| SCOPED_TRACE(testing::Message() << "Test input: " << test_input); |
| |
| // Verify that `origin` is opaque not just because `test_input` results is |
| // an invalid URL (because of a typo in the scheme name, or because of a |
| // technicality like having no host in a noaccess-std-with-host: scheme). |
| EXPECT_TRUE(this->IsValidUrl(test_input)); |
| |
| this->TestUniqueOpaqueOrigin(test_input); |
| } |
| } |
| |
| TYPED_TEST_P(AbstractOriginTest, CustomSchemes_TupleOrigins) { |
| struct TestCase { |
| const char* input; |
| SchemeHostPort expected_tuple; |
| } kTestCases[] = { |
| // Scheme (registered in SetUp()) that's both local and standard. |
| // TODO: Is it really appropriate to do network-host canonicalization of |
| // schemes without ports? |
| {"local-std-with-host:20", {"local-std-with-host", "0.0.0.20", 0}}, |
| {"local-std-with-host:20.", {"local-std-with-host", "0.0.0.20", 0}}, |
| {"local-std-with-host:foo", {"local-std-with-host", "foo", 0}}, |
| {"local-std-with-host://bar:20", {"local-std-with-host", "bar", 0}}, |
| {"local-std-with-host:baz.", {"local-std-with-host", "baz.", 0}}, |
| {"local-std-with-host:baz..", {"local-std-with-host", "baz..", 0}}, |
| {"local-std-with-host:baz..bar", {"local-std-with-host", "baz..bar", 0}}, |
| {"local-std-with-host:baz...", {"local-std-with-host", "baz...", 0}}, |
| |
| // Scheme (registered in SetUp()) that's local but nonstandard. These |
| // always have empty hostnames, but are allowed to be url::Origins. |
| {"local:", {"local", "", 0}}, |
| {"local:foo", {"local", "", 0}}, |
| {"local://bar", {"local", "", 0}}, |
| {"also-local://bar", {"also-local", "", 0}}, |
| |
| {"std-with-host://host", {"std-with-host", "host", 0}}, |
| {"local://host", {"local", "", 0}}, |
| {"local-std-with-host://host", {"local-std-with-host", "host", 0}}, |
| }; |
| |
| for (const TestCase& test : kTestCases) { |
| SCOPED_TRACE(testing::Message() << "Test input: " << test.input); |
| |
| // Only valid URLs should translate into valid, non-opaque origins. |
| EXPECT_TRUE(this->IsValidUrl(test.input)); |
| |
| auto origin = this->CreateOriginFromString(test.input); |
| this->VerifyTupleOriginInvariants(origin, test.expected_tuple); |
| } |
| } |
| |
| REGISTER_TYPED_TEST_SUITE_P(AbstractOriginTest, |
| NonStandardSchemeWithAndroidWebViewHack, |
| OpaqueOriginsFromValidUrls, |
| OpaqueOriginsFromInvalidUrls, |
| TupleOrigins, |
| CustomSchemes_OpaqueOrigins, |
| CustomSchemes_TupleOrigins); |
| |
| } // namespace url |
| |
| #endif // URL_ORIGIN_ABSTRACT_TESTS_H_ |