1 // Copyright 2020 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #ifndef URL_ORIGIN_ABSTRACT_TESTS_H_
6 #define URL_ORIGIN_ABSTRACT_TESTS_H_
10 #include <type_traits>
12 #include "base/containers/contains.h"
13 #include "testing/gtest/include/gtest/gtest.h"
15 #include "url/origin.h"
16 #include "url/scheme_host_port.h"
17 #include "url/url_util.h"
21 void ExpectParsedUrlsEqual(const GURL& a, const GURL& b);
23 // AbstractOriginTest below abstracts away differences between url::Origin and
24 // blink::SecurityOrigin by parametrizing the tests with a class that has to
25 // expose the same public members as UrlOriginTestTraits below.
26 class UrlOriginTestTraits {
28 using OriginType = Origin;
30 // Constructing an origin.
31 static OriginType CreateOriginFromString(std::string_view s);
32 static OriginType CreateUniqueOpaqueOrigin();
33 static OriginType CreateWithReferenceOrigin(
35 const OriginType& reference_origin);
36 static OriginType DeriveNewOpaqueOrigin(const OriginType& reference_origin);
38 // Accessors for origin properties.
39 static bool IsOpaque(const OriginType& origin);
40 static std::string GetScheme(const OriginType& origin);
41 static std::string GetHost(const OriginType& origin);
42 static uint16_t GetPort(const OriginType& origin);
43 static SchemeHostPort GetTupleOrPrecursorTupleIfOpaque(
44 const OriginType& origin);
46 // Wrappers for other instance methods of OriginType.
47 static bool IsSameOrigin(const OriginType& a, const OriginType& b);
48 static std::string Serialize(const OriginType& origin);
50 // "Accessors" of URL properties.
52 // TODO(lukasza): Consider merging together OriginTraitsBase here and
53 // UrlTraitsBase in //url/gurl_abstract_tests.h.
54 static bool IsValidUrl(std::string_view str);
56 // Only static members = no constructors are needed.
57 UrlOriginTestTraits() = delete;
60 // Test suite for tests that cover both url::Origin and blink::SecurityOrigin.
61 template <typename TOriginTraits>
62 class AbstractOriginTest : public testing::Test {
64 void SetUp() override {
65 const char* kSchemesToRegister[] = {
68 "noaccess-std-with-host",
71 "local-std-with-host",
72 "local-noaccess-std-with-host",
78 for (const char* kScheme : kSchemesToRegister) {
79 std::string scheme(kScheme);
80 if (base::Contains(scheme, "noaccess"))
81 AddNoAccessScheme(kScheme);
82 if (base::Contains(scheme, "std-with-host"))
83 AddStandardScheme(kScheme, SchemeType::SCHEME_WITH_HOST);
84 if (base::Contains(scheme, "local"))
85 AddLocalScheme(kScheme);
86 if (base::Contains(scheme, "sec"))
87 AddSecureScheme(kScheme);
92 // Wrappers that help ellide away TOriginTraits.
94 // Note that calling the wrappers needs to be prefixed with `this->...` to
95 // avoid hitting: explicit qualification required to use member 'IsOpaque'
96 // from dependent base class.
97 using OriginType = typename TOriginTraits::OriginType;
98 OriginType CreateOriginFromString(std::string_view s) {
99 return TOriginTraits::CreateOriginFromString(s);
101 OriginType CreateUniqueOpaqueOrigin() {
102 return TOriginTraits::CreateUniqueOpaqueOrigin();
104 OriginType CreateWithReferenceOrigin(std::string_view url,
105 const OriginType& reference_origin) {
106 return TOriginTraits::CreateWithReferenceOrigin(url, reference_origin);
108 OriginType DeriveNewOpaqueOrigin(const OriginType& reference_origin) {
109 return TOriginTraits::DeriveNewOpaqueOrigin(reference_origin);
111 bool IsOpaque(const OriginType& origin) {
112 return TOriginTraits::IsOpaque(origin);
114 std::string GetScheme(const OriginType& origin) {
115 return TOriginTraits::GetScheme(origin);
117 std::string GetHost(const OriginType& origin) {
118 return TOriginTraits::GetHost(origin);
120 uint16_t GetPort(const OriginType& origin) {
121 return TOriginTraits::GetPort(origin);
123 SchemeHostPort GetTupleOrPrecursorTupleIfOpaque(const OriginType& origin) {
124 return TOriginTraits::GetTupleOrPrecursorTupleIfOpaque(origin);
126 bool IsSameOrigin(const OriginType& a, const OriginType& b) {
127 bool is_a_same_with_b = TOriginTraits::IsSameOrigin(a, b);
128 bool is_b_same_with_a = TOriginTraits::IsSameOrigin(b, a);
129 EXPECT_EQ(is_a_same_with_b, is_b_same_with_a);
130 return is_a_same_with_b;
132 std::string Serialize(const OriginType& origin) {
133 return TOriginTraits::Serialize(origin);
135 bool IsValidUrl(std::string_view str) {
136 return TOriginTraits::IsValidUrl(str);
139 #define EXPECT_SAME_ORIGIN(a, b) \
140 EXPECT_TRUE(this->IsSameOrigin((a), (b))) \
141 << "When checking if \"" << this->Serialize(a) << "\" is " \
142 << "same-origin with \"" << this->Serialize(b) << "\""
144 #define EXPECT_CROSS_ORIGIN(a, b) \
145 EXPECT_FALSE(this->IsSameOrigin((a), (b))) \
146 << "When checking if \"" << this->Serialize(a) << "\" is " \
147 << "cross-origin from \"" << this->Serialize(b) << "\""
149 void VerifyOriginInvariants(const OriginType& origin) {
150 // An origin is always same-origin with itself.
151 EXPECT_SAME_ORIGIN(origin, origin);
153 // A copy of |origin| should be same-origin as well.
154 auto origin_copy = origin;
155 EXPECT_EQ(this->GetScheme(origin), this->GetScheme(origin_copy));
156 EXPECT_EQ(this->GetHost(origin), this->GetHost(origin_copy));
157 EXPECT_EQ(this->GetPort(origin), this->GetPort(origin_copy));
158 EXPECT_EQ(this->IsOpaque(origin), this->IsOpaque(origin_copy));
159 EXPECT_SAME_ORIGIN(origin, origin_copy);
161 // An origin is always cross-origin from another, unique, opaque origin.
162 EXPECT_CROSS_ORIGIN(origin, this->CreateUniqueOpaqueOrigin());
164 // An origin is always cross-origin from another tuple origin.
165 auto different_tuple_origin =
166 this->CreateOriginFromString("https://not-in-the-list.test/");
167 EXPECT_CROSS_ORIGIN(origin, different_tuple_origin);
169 // Deriving an origin for "about:blank".
170 auto about_blank_origin1 =
171 this->CreateWithReferenceOrigin("about:blank", origin);
172 auto about_blank_origin2 =
173 this->CreateWithReferenceOrigin("about:blank?bar#foo", origin);
174 EXPECT_SAME_ORIGIN(origin, about_blank_origin1);
175 EXPECT_SAME_ORIGIN(origin, about_blank_origin2);
177 // Derived opaque origins.
178 std::vector<OriginType> derived_origins = {
179 this->DeriveNewOpaqueOrigin(origin),
180 this->CreateWithReferenceOrigin("data:text/html,baz", origin),
181 this->DeriveNewOpaqueOrigin(about_blank_origin1),
183 for (size_t i = 0; i < derived_origins.size(); i++) {
184 SCOPED_TRACE(testing::Message() << "Derived origin #" << i);
185 const OriginType& derived_origin = derived_origins[i];
186 EXPECT_TRUE(this->IsOpaque(derived_origin));
187 EXPECT_SAME_ORIGIN(derived_origin, derived_origin);
188 EXPECT_CROSS_ORIGIN(origin, derived_origin);
189 EXPECT_EQ(this->GetTupleOrPrecursorTupleIfOpaque(origin),
190 this->GetTupleOrPrecursorTupleIfOpaque(derived_origin));
194 void VerifyUniqueOpaqueOriginInvariants(const OriginType& origin) {
195 if (!this->IsOpaque(origin)) {
196 ADD_FAILURE() << "Got unexpectedly non-opaque origin: "
197 << this->Serialize(origin);
198 return; // Skip other test assertions.
201 // Opaque origins should have an "empty" scheme, host and port.
202 EXPECT_EQ("", this->GetScheme(origin));
203 EXPECT_EQ("", this->GetHost(origin));
204 EXPECT_EQ(0, this->GetPort(origin));
206 // Unique opaque origins should have an empty precursor tuple.
207 EXPECT_EQ(SchemeHostPort(), this->GetTupleOrPrecursorTupleIfOpaque(origin));
209 // Serialization test.
210 EXPECT_EQ("null", this->Serialize(origin));
212 // Invariants that should hold for any origin.
213 VerifyOriginInvariants(origin);
216 void TestUniqueOpaqueOrigin(std::string_view test_input) {
217 auto origin = this->CreateOriginFromString(test_input);
218 this->VerifyUniqueOpaqueOriginInvariants(origin);
220 // Re-creating from the URL should be cross-origin.
221 auto origin_recreated_from_same_input =
222 this->CreateOriginFromString(test_input);
223 EXPECT_CROSS_ORIGIN(origin, origin_recreated_from_same_input);
226 void VerifyTupleOriginInvariants(const OriginType& origin,
227 const SchemeHostPort& expected_tuple) {
228 if (this->IsOpaque(origin)) {
229 ADD_FAILURE() << "Got unexpectedly opaque origin";
230 return; // Skip other test assertions.
232 SCOPED_TRACE(testing::Message()
233 << "Actual origin: " << this->Serialize(origin));
235 // Compare `origin` against the `expected_tuple`.
236 EXPECT_EQ(expected_tuple.scheme(), this->GetScheme(origin));
237 EXPECT_EQ(expected_tuple.host(), this->GetHost(origin));
238 EXPECT_EQ(expected_tuple.port(), this->GetPort(origin));
239 EXPECT_EQ(expected_tuple, this->GetTupleOrPrecursorTupleIfOpaque(origin));
241 // Serialization test.
243 // TODO(lukasza): Consider preserving the hostname when serializing file:
244 // URLs. Dropping the hostname seems incompatible with section 6 of
245 // rfc6454. Even though section 4 says that "the implementation MAY
246 // return an implementation-defined value", it seems that Chromium
247 // implementation *does* include the hostname in the origin SchemeHostPort
249 if (expected_tuple.scheme() != kFileScheme || expected_tuple.host() == "") {
250 EXPECT_SAME_ORIGIN(origin,
251 this->CreateOriginFromString(this->Serialize(origin)));
254 // Invariants that should hold for any origin.
255 VerifyOriginInvariants(origin);
259 ScopedSchemeRegistryForTests scoped_scheme_registry_;
262 TYPED_TEST_SUITE_P(AbstractOriginTest);
264 TYPED_TEST_P(AbstractOriginTest, NonStandardSchemeWithAndroidWebViewHack) {
265 EnableNonStandardSchemesForAndroidWebView();
267 // Regression test for https://crbug.com/896059.
268 auto origin = this->CreateOriginFromString("unknown-scheme://");
269 EXPECT_FALSE(this->IsOpaque(origin));
270 EXPECT_EQ("unknown-scheme", this->GetScheme(origin));
271 EXPECT_EQ("", this->GetHost(origin));
272 EXPECT_EQ(0, this->GetPort(origin));
274 // about:blank translates into an opaque origin, even in presence of
275 // EnableNonStandardSchemesForAndroidWebView.
276 origin = this->CreateOriginFromString("about:blank");
277 EXPECT_TRUE(this->IsOpaque(origin));
280 TYPED_TEST_P(AbstractOriginTest, OpaqueOriginsFromValidUrls) {
281 const char* kTestCases[] = {
282 // Built-in noaccess schemes.
283 "data:text/html,Hello!",
284 "javascript:alert(1)",
288 "blob:null/foo", // blob:null (actually a valid URL)
289 "blob:data:foo", // blob + data (which is nonstandard)
290 "blob:about://blank/", // blob + about (which is nonstandard)
291 "blob:about:blank/", // blob + about (which is nonstandard)
292 "blob:blob:http://www.example.com/guid-goes-here",
293 "blob:filesystem:ws:b/.",
294 "blob:filesystem:ftp://a/b",
295 "blob:blob:file://localhost/foo/bar",
298 for (const char* test_input : kTestCases) {
299 SCOPED_TRACE(testing::Message() << "Test input: " << test_input);
301 // Verify that `origin` is opaque not just because `test_input` results is
302 // an invalid URL (because of a typo in the scheme name, or because of a
303 // technicality like having no host in a noaccess-std-with-host: scheme).
304 EXPECT_TRUE(this->IsValidUrl(test_input));
306 this->TestUniqueOpaqueOrigin(test_input);
310 TYPED_TEST_P(AbstractOriginTest, OpaqueOriginsFromInvalidUrls) {
311 // TODO(lukasza): Consider moving those to GURL/KURL tests that verify what
312 // inputs are parsed as an invalid URL.
314 const char* kTestCases[] = {
315 // Invalid file: URLs.
316 "file://example.com:443/etc/passwd", // No port expected.
318 // Invalid HTTP URLs.
325 "http::///invalid.example.com/",
326 "http://example.com:65536/", // Port out of range.
327 "http://example.com:-1/", // Port out of range.
328 "http://example.com:18446744073709551616/", // Port = 2^64.
329 "http://example.com:18446744073709551616999/", // Lots of port digits.
331 // Invalid filesystem URLs.
332 "filesystem:http://example.com/", // Missing /type/.
333 "filesystem:local:baz./type/",
334 "filesystem:local://hostname/type/",
335 "filesystem:unknown-scheme://hostname/type/",
336 "filesystem:filesystem:http://example.org:88/foo/bar",
338 // Invalid IP addresses
340 "http://[2001:0db8:0000:0000:0000:0000:0000:0000:0001]/", // 9 groups.
342 // Unknown scheme without a colon character (":") gives an invalid URL.
345 // Standard schemes require a hostname (and result in an opaque origin if
346 // the hostname is missing).
347 "local-std-with-host:",
348 "noaccess-std-with-host:",
351 for (const char* test_input : kTestCases) {
352 SCOPED_TRACE(testing::Message() << "Test input: " << test_input);
354 // All testcases here are expected to represent invalid URLs.
355 // an invalid URL (because of a type in scheme name, or because of a
356 // technicality like having no host in a noaccess-std-with-host: scheme).
357 EXPECT_FALSE(this->IsValidUrl(test_input));
359 // Invalid URLs should always result in an opaque origin.
360 this->TestUniqueOpaqueOrigin(test_input);
364 TYPED_TEST_P(AbstractOriginTest, TupleOrigins) {
367 SchemeHostPort expected_tuple;
370 {"file:///etc/passwd", {"file", "", 0}},
371 {"file://example.com/etc/passwd", {"file", "example.com", 0}},
372 {"file:///", {"file", "", 0}},
373 {"file://hostname/C:/dir/file.txt", {"file", "hostname", 0}},
376 {"http://example.com/", {"http", "example.com", 80}},
377 {"http://example.com:80/", {"http", "example.com", 80}},
378 {"http://example.com:123/", {"http", "example.com", 123}},
379 {"http://example.com:0/", {"http", "example.com", 0}},
380 {"http://example.com:65535/", {"http", "example.com", 65535}},
381 {"https://example.com/", {"https", "example.com", 443}},
382 {"https://example.com:443/", {"https", "example.com", 443}},
383 {"https://example.com:123/", {"https", "example.com", 123}},
384 {"https://example.com:0/", {"https", "example.com", 0}},
385 {"https://example.com:65535/", {"https", "example.com", 65535}},
386 {"http://user:pass@example.com/", {"http", "example.com", 80}},
387 {"http://example.com:123/?query", {"http", "example.com", 123}},
388 {"https://example.com/#1234", {"https", "example.com", 443}},
389 {"https://u:p@example.com:123/?query#1234",
390 {"https", "example.com", 123}},
391 {"http://example/", {"http", "example", 80}},
394 {"blob:http://example.com/guid-goes-here", {"http", "example.com", 80}},
395 {"blob:http://example.com:123/guid-goes-here",
396 {"http", "example.com", 123}},
397 {"blob:https://example.com/guid-goes-here",
398 {"https", "example.com", 443}},
399 {"blob:http://u:p@example.com/guid-goes-here",
400 {"http", "example.com", 80}},
403 {"filesystem:http://example.com/type/", {"http", "example.com", 80}},
404 {"filesystem:http://example.com:123/type/", {"http", "example.com", 123}},
405 {"filesystem:https://example.com/type/", {"https", "example.com", 443}},
406 {"filesystem:https://example.com:123/type/",
407 {"https", "example.com", 123}},
408 {"filesystem:local-std-with-host:baz./type/",
409 {"local-std-with-host", "baz.", 0}},
412 {"http://192.168.9.1/", {"http", "192.168.9.1", 80}},
413 {"http://[2001:db8::1]/", {"http", "[2001:db8::1]", 80}},
414 {"http://[2001:0db8:0000:0000:0000:0000:0000:0001]/",
415 {"http", "[2001:db8::1]", 80}},
416 {"http://1/", {"http", "0.0.0.1", 80}},
417 {"http://1:1/", {"http", "0.0.0.1", 1}},
418 {"http://3232237825/", {"http", "192.168.9.1", 80}},
421 {"http://☃.net/", {"http", "xn--n3h.net", 80}},
422 {"blob:http://☃.net/", {"http", "xn--n3h.net", 80}},
423 {"local-std-with-host:↑↑↓↓←→←→ba.↑↑↓↓←→←→ba.0.bg",
424 {"local-std-with-host", "xn--ba-rzuadaibfa.xn--ba-rzuadaibfa.0.bg", 0}},
427 {"ftp://example.com/", {"ftp", "example.com", 21}},
428 {"ws://example.com/", {"ws", "example.com", 80}},
429 {"wss://example.com/", {"wss", "example.com", 443}},
430 {"wss://user:pass@example.com/", {"wss", "example.com", 443}},
433 for (const TestCase& test : kTestCases) {
434 SCOPED_TRACE(testing::Message() << "Test input: " << test.input);
436 // Only valid URLs should translate into valid, non-opaque origins.
437 EXPECT_TRUE(this->IsValidUrl(test.input));
439 auto origin = this->CreateOriginFromString(test.input);
440 this->VerifyTupleOriginInvariants(origin, test.expected_tuple);
444 TYPED_TEST_P(AbstractOriginTest, CustomSchemes_OpaqueOrigins) {
445 const char* kTestCases[] = {
447 "unknown-scheme:foo",
448 "unknown-scheme://bar",
450 // Unknown scheme that is a prefix or suffix of a registered scheme.
456 // Custom no-access schemes translate into an opaque origin (just like the
457 // built-in no-access schemes such as about:blank or data:).
458 "noaccess-std-with-host:foo",
459 "noaccess-std-with-host://bar",
461 "local-noaccess://host",
462 "local-noaccess-std-with-host://host",
465 for (const char* test_input : kTestCases) {
466 SCOPED_TRACE(testing::Message() << "Test input: " << test_input);
468 // Verify that `origin` is opaque not just because `test_input` results is
469 // an invalid URL (because of a typo in the scheme name, or because of a
470 // technicality like having no host in a noaccess-std-with-host: scheme).
471 EXPECT_TRUE(this->IsValidUrl(test_input));
473 this->TestUniqueOpaqueOrigin(test_input);
477 TYPED_TEST_P(AbstractOriginTest, CustomSchemes_TupleOrigins) {
480 SchemeHostPort expected_tuple;
482 // Scheme (registered in SetUp()) that's both local and standard.
483 // TODO: Is it really appropriate to do network-host canonicalization of
484 // schemes without ports?
485 {"local-std-with-host:20", {"local-std-with-host", "0.0.0.20", 0}},
486 {"local-std-with-host:20.", {"local-std-with-host", "0.0.0.20", 0}},
487 {"local-std-with-host:foo", {"local-std-with-host", "foo", 0}},
488 {"local-std-with-host://bar:20", {"local-std-with-host", "bar", 0}},
489 {"local-std-with-host:baz.", {"local-std-with-host", "baz.", 0}},
490 {"local-std-with-host:baz..", {"local-std-with-host", "baz..", 0}},
491 {"local-std-with-host:baz..bar", {"local-std-with-host", "baz..bar", 0}},
492 {"local-std-with-host:baz...", {"local-std-with-host", "baz...", 0}},
494 // Scheme (registered in SetUp()) that's local but nonstandard. These
495 // always have empty hostnames, but are allowed to be url::Origins.
496 {"local:", {"local", "", 0}},
497 {"local:foo", {"local", "", 0}},
498 {"local://bar", {"local", "", 0}},
499 {"also-local://bar", {"also-local", "", 0}},
501 {"std-with-host://host", {"std-with-host", "host", 0}},
502 {"local://host", {"local", "", 0}},
503 {"local-std-with-host://host", {"local-std-with-host", "host", 0}},
506 for (const TestCase& test : kTestCases) {
507 SCOPED_TRACE(testing::Message() << "Test input: " << test.input);
509 // Only valid URLs should translate into valid, non-opaque origins.
510 EXPECT_TRUE(this->IsValidUrl(test.input));
512 auto origin = this->CreateOriginFromString(test.input);
513 this->VerifyTupleOriginInvariants(origin, test.expected_tuple);
517 REGISTER_TYPED_TEST_SUITE_P(AbstractOriginTest,
518 NonStandardSchemeWithAndroidWebViewHack,
519 OpaqueOriginsFromValidUrls,
520 OpaqueOriginsFromInvalidUrls,
522 CustomSchemes_OpaqueOrigins,
523 CustomSchemes_TupleOrigins);
527 #endif // URL_ORIGIN_ABSTRACT_TESTS_H_