// Copyright (c) 2017 Cloudflare, Inc. and contributors // Licensed under the MIT License: // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #include "url.h" #include <kj/debug.h> #include <kj/test.h> namespace kj { namespace { Url parseAndCheck(kj::StringPtr originalText, kj::StringPtr expectedRestringified = nullptr) { if (expectedRestringified == nullptr) expectedRestringified = originalText; auto url = Url::parse(originalText); KJ_EXPECT(kj::str(url) == expectedRestringified, url, originalText, expectedRestringified); return url; } KJ_TEST("parse / stringify URL") { { auto url = parseAndCheck("https://capnproto.org"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://capnproto.org:80"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org:80"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://capnproto.org/"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://capnproto.org/foo/bar"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://capnproto.org/foo/bar/"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux&corge#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_ASSERT(url.query.size() == 2); KJ_EXPECT(url.query[0].name == "baz"); KJ_EXPECT(url.query[0].value == "qux"); KJ_EXPECT(url.query[1].name == "corge"); KJ_EXPECT(url.query[1].value == nullptr); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org/foo/bar/?baz=qux&corge=grault#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(url.hasTrailingSlash); KJ_ASSERT(url.query.size() == 2); KJ_EXPECT(url.query[0].name == "baz"); KJ_EXPECT(url.query[0].value == "qux"); KJ_EXPECT(url.query[1].name == "corge"); KJ_EXPECT(url.query[1].value == "grault"); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_ASSERT(url.query.size() == 1); KJ_EXPECT(url.query[0].name == "baz"); KJ_EXPECT(url.query[0].value == "qux"); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org/foo?bar%20baz=qux+quux", "https://capnproto.org/foo?bar+baz=qux+quux"); KJ_ASSERT(url.query.size() == 1); KJ_EXPECT(url.query[0].name == "bar baz"); KJ_EXPECT(url.query[0].value == "qux quux"); } { auto url = parseAndCheck("https://capnproto.org/foo/bar#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org/foo/bar/#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://capnproto.org/#garply"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply"); } { auto url = parseAndCheck("https://foo@capnproto.org"); KJ_EXPECT(url.scheme == "https"); auto& user = KJ_ASSERT_NONNULL(url.userInfo); KJ_EXPECT(user.username == "foo"); KJ_EXPECT(user.password == nullptr); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://foo:1234@capnproto.org"); KJ_EXPECT(url.scheme == "https"); auto& user = KJ_ASSERT_NONNULL(url.userInfo); KJ_EXPECT(user.username == "foo"); KJ_EXPECT(KJ_ASSERT_NONNULL(user.password) == "1234"); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path == nullptr); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } { auto url = parseAndCheck("https://[2001:db8::1234]:80/foo"); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.userInfo == nullptr); KJ_EXPECT(url.host == "[2001:db8::1234]:80"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_EXPECT(url.query == nullptr); KJ_EXPECT(url.fragment == nullptr); } parseAndCheck("https://capnproto.org/foo/bar?", "https://capnproto.org/foo/bar"); parseAndCheck("https://capnproto.org/foo/bar?#", "https://capnproto.org/foo/bar#"); parseAndCheck("https://capnproto.org/foo/bar#"); // Scheme and host are forced to lower-case. parseAndCheck("hTtP://capNprotO.org/fOo/bAr", "http://capnproto.org/fOo/bAr"); } KJ_TEST("URL percent encoding") { parseAndCheck( "https://b%6fb:%61bcd@capnpr%6fto.org/f%6fo?b%61r=b%61z#q%75x", "https://bob:abcd@capnproto.org/foo?bar=baz#qux"); parseAndCheck( "https://b\001b:\001bcd@capnproto.org/f\001o?b\001r=b\001z#q\001x", "https://b%01b:%01bcd@capnproto.org/f%01o?b%01r=b%01z#q%01x"); parseAndCheck( "https://b b: bcd@capnproto.org/f o?b r=b z#q x", "https://b%20b:%20bcd@capnproto.org/f%20o?b+r=b+z#q%20x"); } KJ_TEST("URL relative paths") { parseAndCheck( "https://capnproto.org/foo//bar", "https://capnproto.org/foo/bar"); parseAndCheck( "https://capnproto.org/foo/./bar", "https://capnproto.org/foo/bar"); parseAndCheck( "https://capnproto.org/foo/bar//", "https://capnproto.org/foo/bar/"); parseAndCheck( "https://capnproto.org/foo/bar/.", "https://capnproto.org/foo/bar/"); parseAndCheck( "https://capnproto.org/foo/baz/../bar", "https://capnproto.org/foo/bar"); parseAndCheck( "https://capnproto.org/foo/bar/baz/..", "https://capnproto.org/foo/bar/"); parseAndCheck( "https://capnproto.org/..", "https://capnproto.org/"); parseAndCheck( "https://capnproto.org/foo/../..", "https://capnproto.org/"); } KJ_TEST("URL for HTTP request") { { Url url = Url::parse("https://bob:1234@capnproto.org/foo/bar?baz=qux#corge"); KJ_EXPECT(url.toString(Url::REMOTE_HREF) == "https://bob:1234@capnproto.org/foo/bar?baz=qux#corge"); KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/foo/bar?baz=qux"); KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/foo/bar?baz=qux"); } { Url url = Url::parse("https://capnproto.org"); KJ_EXPECT(url.toString(Url::REMOTE_HREF) == "https://capnproto.org"); KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org"); KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/"); } { Url url = Url::parse("/foo/bar?baz=qux&corge", Url::HTTP_REQUEST); KJ_EXPECT(url.scheme == nullptr); KJ_EXPECT(url.host == nullptr); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_ASSERT(url.query.size() == 2); KJ_EXPECT(url.query[0].name == "baz"); KJ_EXPECT(url.query[0].value == "qux"); KJ_EXPECT(url.query[1].name == "corge"); KJ_EXPECT(url.query[1].value == nullptr); } { Url url = Url::parse("https://capnproto.org/foo/bar?baz=qux&corge", Url::HTTP_PROXY_REQUEST); KJ_EXPECT(url.scheme == "https"); KJ_EXPECT(url.host == "capnproto.org"); KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"})); KJ_EXPECT(!url.hasTrailingSlash); KJ_ASSERT(url.query.size() == 2); KJ_EXPECT(url.query[0].name == "baz"); KJ_EXPECT(url.query[0].value == "qux"); KJ_EXPECT(url.query[1].name == "corge"); KJ_EXPECT(url.query[1].value == nullptr); } } KJ_TEST("parse URL failure") { KJ_EXPECT(Url::tryParse("ht/tps://capnproto.org") == nullptr); KJ_EXPECT(Url::tryParse("capnproto.org") == nullptr); KJ_EXPECT(Url::tryParse("https:foo") == nullptr); // percent-decode errors KJ_EXPECT(Url::tryParse("https://capnproto.org/f%nno") == nullptr); KJ_EXPECT(Url::tryParse("https://capnproto.org/foo?b%nnr=baz") == nullptr); // components not valid in context KJ_EXPECT(Url::tryParse("https://capnproto.org/foo", Url::HTTP_REQUEST) == nullptr); KJ_EXPECT(Url::tryParse("/foo#bar", Url::HTTP_REQUEST) == nullptr); KJ_EXPECT(Url::tryParse("https://bob:123@capnproto.org/foo", Url::HTTP_PROXY_REQUEST) == nullptr); KJ_EXPECT(Url::tryParse("https://capnproto.org/foo#bar", Url::HTTP_PROXY_REQUEST) == nullptr); } void parseAndCheckRelative(kj::StringPtr base, kj::StringPtr relative, kj::StringPtr expected) { auto parsed = Url::parse(base).parseRelative(relative); KJ_EXPECT(kj::str(parsed) == expected, parsed, expected); } KJ_TEST("parse relative URL") { parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "#grault", "https://capnproto.org/foo/bar?baz=qux#grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "?grault", "https://capnproto.org/foo/bar?grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "?grault+garply=waldo", "https://capnproto.org/foo/bar?grault+garply=waldo"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "grault", "https://capnproto.org/foo/grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "/grault", "https://capnproto.org/grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "//grault", "https://grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "//grault/garply", "https://grault/garply"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "http:/grault", "http://capnproto.org/grault"); parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge", "/http:/grault", "https://capnproto.org/http%3A/grault"); } } // namespace } // namespace kj