From 90340c24c4ada90617b75b1c9b9b67859462f28c Mon Sep 17 00:00:00 2001 From: "Mark D. Roth" Date: Mon, 10 Jan 2022 15:48:17 -0800 Subject: [PATCH] URI parser: fix percent-encoding and add ToString() method (#28485) * fix URI percent-encoding character set * clean up and fix edge cases * add ToString() method to URI parser with appropriate percent-encoding logic * clang-format * fix ordering for URI::QueryParam * fix decoding edge cases in parsing * generate upper-case hex digits, as per the RFC * clang-format * reuse offset variable --- .../resolver/sockaddr/sockaddr_resolver.cc | 4 +- .../resolver/xds/xds_resolver.cc | 14 +- src/core/lib/uri/uri_parser.cc | 278 +++++++++---- src/core/lib/uri/uri_parser.h | 58 ++- test/core/uri/uri_parser_test.cc | 393 ++++++++++++++---- 5 files changed, 569 insertions(+), 178 deletions(-) diff --git a/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc b/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc index 9814cf2f392..05fc5e38ef9 100644 --- a/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc +++ b/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc @@ -91,9 +91,9 @@ bool ParseUri(const URI& uri, // Construct addresses. bool errors_found = false; for (absl::string_view ith_path : absl::StrSplit(uri.path(), ',')) { - URI ith_uri(uri.scheme(), "", std::string(ith_path), {}, ""); + auto ith_uri = URI::Create(uri.scheme(), "", std::string(ith_path), {}, ""); grpc_resolved_address addr; - if (!parse(ith_uri, &addr)) { + if (!ith_uri.ok() || !parse(*ith_uri, &addr)) { errors_found = true; break; } diff --git a/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc b/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc index 0e85e3312f1..b669a9163ff 100644 --- a/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc +++ b/src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc @@ -81,11 +81,10 @@ class XdsResolver : public Resolver { uri_(std::move(args.uri)), data_plane_authority_(GetDataPlaneAuthority(*args.args, uri_)) { if (GRPC_TRACE_FLAG_ENABLED(grpc_xds_resolver_trace)) { - gpr_log(GPR_INFO, - "[xds_resolver %p] created for URI scheme %s path %s authority " - "%s data plane authority %s", - this, args.uri.scheme().c_str(), args.uri.path().c_str(), - args.uri.authority().c_str(), data_plane_authority_.c_str()); + gpr_log( + GPR_INFO, + "[xds_resolver %p] created for URI %s; data plane authority is %s", + this, uri_.ToString().c_str(), data_plane_authority_.c_str()); } } @@ -738,7 +737,8 @@ void XdsResolver::StartLocked() { "/envoy.config.listener.v3.Listener/%s"); } lds_resource_name_ = absl::StrReplaceAll( - name_template, {{"%s", URI::PercentEncode(resource_name_fragment)}}); + name_template, + {{"%s", URI::PercentEncodePath(resource_name_fragment)}}); } else { // target_uri.authority not set absl::string_view name_template = @@ -748,7 +748,7 @@ void XdsResolver::StartLocked() { name_template = "%s"; } if (absl::StartsWith(name_template, "xdstp:")) { - resource_name_fragment = URI::PercentEncode(resource_name_fragment); + resource_name_fragment = URI::PercentEncodePath(resource_name_fragment); } lds_resource_name_ = absl::StrReplaceAll(name_template, {{"%s", resource_name_fragment}}); diff --git a/src/core/lib/uri/uri_parser.cc b/src/core/lib/uri/uri_parser.cc index 67f0bb9b342..f2806203470 100644 --- a/src/core/lib/uri/uri_parser.cc +++ b/src/core/lib/uri/uri_parser.cc @@ -1,20 +1,18 @@ -/* - * - * Copyright 2015 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ +// +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// #include @@ -37,48 +35,110 @@ namespace grpc_core { namespace { -bool ShouldEscape(unsigned char c) { - // Unreserved characters RFC 3986 section 2.3 Unreserved Characters. - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9')) { - return false; +// Returns true for any sub-delim character, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 +bool IsSubDelimChar(char c) { + switch (c) { + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + return true; } + return false; +} + +// Returns true for any unreserved character, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 +bool IsUnreservedChar(char c) { + if (absl::ascii_isalnum(c)) return true; switch (c) { case '-': - case '_': case '.': + case '_': case '~': - case '/': + return true; + } + return false; +} + +// Returns true for any character in scheme, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-3.1 +bool IsSchemeChar(char c) { + if (absl::ascii_isalnum(c)) return true; + switch (c) { + case '+': + case '-': + case '.': + return true; + } + return false; +} + +// Returns true for any character in authority, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-3.2 +bool IsAuthorityChar(char c) { + if (IsUnreservedChar(c)) return true; + if (IsSubDelimChar(c)) return true; + switch (c) { case ':': - return false; + case '[': + case ']': + case '@': + return true; } - return true; + return false; } -// Checks if this string is made up of pchars, '/', '?', and '%' exclusively. +// Returns true for any character in pchar, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 +bool IsPChar(char c) { + if (IsUnreservedChar(c)) return true; + if (IsSubDelimChar(c)) return true; + switch (c) { + case ':': + case '@': + return true; + } + return false; +} + +// Returns true for any character allowed in a URI path, as defined in: +// https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 +bool IsPathChar(char c) { return IsPChar(c) || c == '/'; } + +// Returns true for any character allowed in a URI query or fragment, +// as defined in: // See https://tools.ietf.org/html/rfc3986#section-3.4 -bool IsPCharString(absl::string_view str) { - return (str.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789" - "?/:@\\-._~!$&'()*+,;=%") == - absl::string_view::npos); +bool IsQueryOrFragmentChar(char c) { + return IsPChar(c) || c == '/' || c == '?'; } -absl::Status MakeInvalidURIStatus(absl::string_view part_name, - absl::string_view uri, - absl::string_view extra) { - return absl::InvalidArgumentError(absl::StrFormat( - "Could not parse '%s' from uri '%s'. %s", part_name, uri, extra)); +// Same as IsQueryOrFragmentChar(), but excludes '&' and '='. +bool IsQueryKeyOrValueChar(char c) { + return c != '&' && c != '=' && IsQueryOrFragmentChar(c); } -} // namespace -std::string URI::PercentEncode(absl::string_view str) { +// Returns a copy of str, percent-encoding any character for which +// is_allowed_char() returns false. +std::string PercentEncode(absl::string_view str, + std::function is_allowed_char) { std::string out; - for (const char c : str) { - if (ShouldEscape(c)) { + for (char c : str) { + if (!is_allowed_char(c)) { std::string hex = absl::BytesToHexString(absl::string_view(&c, 1)); GPR_ASSERT(hex.size() == 2); + // BytesToHexString() returns lower case, but + // https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1 says + // to prefer upper-case. + absl::AsciiStrToUpper(&hex); out.push_back('%'); out.append(hex); } else { @@ -88,6 +148,28 @@ std::string URI::PercentEncode(absl::string_view str) { return out; } +// Checks if this string is made up of query/fragment chars and '%' exclusively. +// See https://tools.ietf.org/html/rfc3986#section-3.4 +bool IsQueryOrFragmentString(absl::string_view str) { + for (char c : str) { + if (!IsQueryOrFragmentChar(c) && c != '%') return false; + } + return true; +} + +absl::Status MakeInvalidURIStatus(absl::string_view part_name, + absl::string_view uri, + absl::string_view extra) { + return absl::InvalidArgumentError(absl::StrFormat( + "Could not parse '%s' from uri '%s'. %s", part_name, uri, extra)); +} + +} // namespace + +std::string URI::PercentEncodePath(absl::string_view str) { + return PercentEncode(str, IsPathChar); +} + // Similar to `grpc_permissive_percent_decode_slice`, this %-decodes all valid // triplets, and passes through the rest verbatim. std::string URI::PercentDecode(absl::string_view str) { @@ -99,18 +181,14 @@ std::string URI::PercentDecode(absl::string_view str) { out.reserve(str.size()); for (size_t i = 0; i < str.length(); i++) { unescaped = ""; - if (str[i] != '%') { - out += str[i]; - continue; - } - if (i + 3 >= str.length() || - !absl::CUnescape(absl::StrCat("\\x", str.substr(i + 1, 2)), - &unescaped) || - unescaped.length() > 1) { - out += str[i]; - } else { + if (str[i] == '%' && i + 3 <= str.length() && + absl::CUnescape(absl::StrCat("\\x", str.substr(i + 1, 2)), + &unescaped) && + unescaped.length() == 1) { out += unescaped[0]; i += 2; + } else { + out += str[i]; } } return out; @@ -120,11 +198,11 @@ absl::StatusOr URI::Parse(absl::string_view uri_text) { absl::StatusOr decoded; absl::string_view remaining = uri_text; // parse scheme - size_t idx = remaining.find(':'); - if (idx == remaining.npos || idx == 0) { + size_t offset = remaining.find(':'); + if (offset == remaining.npos || offset == 0) { return MakeInvalidURIStatus("scheme", uri_text, "Scheme not found."); } - std::string scheme(remaining.substr(0, idx)); + std::string scheme(remaining.substr(0, offset)); if (scheme.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+-.") != std::string::npos) { @@ -136,30 +214,38 @@ absl::StatusOr URI::Parse(absl::string_view uri_text) { "scheme", uri_text, "Scheme must begin with an alpha character [A-Za-z]."); } - remaining.remove_prefix(scheme.length() + 1); + remaining.remove_prefix(offset + 1); // parse authority std::string authority; - if (absl::StartsWith(remaining, "//")) { - remaining.remove_prefix(2); - authority = - PercentDecode(remaining.substr(0, remaining.find_first_of("/?#"))); - remaining.remove_prefix(authority.length()); + if (absl::ConsumePrefix(&remaining, "//")) { + offset = remaining.find_first_of("/?#"); + authority = PercentDecode(remaining.substr(0, offset)); + if (offset == remaining.npos) { + remaining = ""; + } else { + remaining.remove_prefix(offset); + } } // parse path std::string path; if (!remaining.empty()) { - path = PercentDecode(remaining.substr(0, remaining.find_first_of("?#"))); - remaining.remove_prefix(path.length()); + offset = remaining.find_first_of("?#"); + path = PercentDecode(remaining.substr(0, offset)); + if (offset == remaining.npos) { + remaining = ""; + } else { + remaining.remove_prefix(offset); + } } // parse query std::vector query_param_pairs; - if (!remaining.empty() && remaining[0] == '?') { - remaining.remove_prefix(1); - absl::string_view tmp_query = remaining.substr(0, remaining.find('#')); + if (absl::ConsumePrefix(&remaining, "?")) { + offset = remaining.find('#'); + absl::string_view tmp_query = remaining.substr(0, offset); if (tmp_query.empty()) { return MakeInvalidURIStatus("query", uri_text, "Invalid query string."); } - if (!IsPCharString(tmp_query)) { + if (!IsQueryOrFragmentString(tmp_query)) { return MakeInvalidURIStatus("query string", uri_text, "Query string contains invalid characters."); } @@ -170,12 +256,15 @@ absl::StatusOr URI::Parse(absl::string_view uri_text) { query_param_pairs.push_back({PercentDecode(possible_kv.first), PercentDecode(possible_kv.second)}); } - remaining.remove_prefix(tmp_query.length()); + if (offset == remaining.npos) { + remaining = ""; + } else { + remaining.remove_prefix(offset); + } } std::string fragment; - if (!remaining.empty() && remaining[0] == '#') { - remaining.remove_prefix(1); - if (!IsPCharString(remaining)) { + if (absl::ConsumePrefix(&remaining, "#")) { + if (!IsQueryOrFragmentString(remaining)) { return MakeInvalidURIStatus("fragment", uri_text, "Fragment contains invalid characters."); } @@ -185,6 +274,18 @@ absl::StatusOr URI::Parse(absl::string_view uri_text) { std::move(query_param_pairs), std::move(fragment)); } +absl::StatusOr URI::Create(std::string scheme, std::string authority, + std::string path, + std::vector query_parameter_pairs, + std::string fragment) { + if (!authority.empty() && !path.empty() && path[0] != '/') { + return absl::InvalidArgumentError( + "if authority is present, path must start with a '/'"); + } + return URI(std::move(scheme), std::move(authority), std::move(path), + std::move(query_parameter_pairs), std::move(fragment)); +} + URI::URI(std::string scheme, std::string authority, std::string path, std::vector query_parameter_pairs, std::string fragment) : scheme_(std::move(scheme)), @@ -222,4 +323,39 @@ URI& URI::operator=(const URI& other) { } return *this; } + +namespace { + +// A pair formatter for use with absl::StrJoin() for formatting query params. +struct QueryParameterFormatter { + void operator()(std::string* out, const URI::QueryParam& query_param) const { + out->append( + absl::StrCat(PercentEncode(query_param.key, IsQueryKeyOrValueChar), "=", + PercentEncode(query_param.value, IsQueryKeyOrValueChar))); + } +}; + +} // namespace + +std::string URI::ToString() const { + std::vector parts = {PercentEncode(scheme_, IsSchemeChar), ":"}; + if (!authority_.empty()) { + parts.emplace_back("//"); + parts.emplace_back(PercentEncode(authority_, IsAuthorityChar)); + } + if (!path_.empty()) { + parts.emplace_back(PercentEncode(path_, IsPathChar)); + } + if (!query_parameter_pairs_.empty()) { + parts.push_back("?"); + parts.push_back( + absl::StrJoin(query_parameter_pairs_, "&", QueryParameterFormatter())); + } + if (!fragment_.empty()) { + parts.push_back("#"); + parts.push_back(PercentEncode(fragment_, IsQueryOrFragmentChar)); + } + return absl::StrJoin(parts, ""); +} + } // namespace grpc_core diff --git a/src/core/lib/uri/uri_parser.h b/src/core/lib/uri/uri_parser.h index 056fc1a63b4..290d8ea707f 100644 --- a/src/core/lib/uri/uri_parser.h +++ b/src/core/lib/uri/uri_parser.h @@ -1,20 +1,18 @@ -/* - * - * Copyright 2015 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ +// +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// #ifndef GRPC_CORE_LIB_URI_URI_PARSER_H #define GRPC_CORE_LIB_URI_URI_PARSER_H @@ -40,15 +38,23 @@ class URI { bool operator==(const QueryParam& other) const { return key == other.key && value == other.value; } + bool operator<(const QueryParam& other) const { + int c = key.compare(other.key); + if (c != 0) return c < 0; + return value < other.value; + } }; - // Creates an instance of GrpcURI by parsing an rfc3986 URI string. Returns - // an IllegalArgumentError on failure. + // Creates a URI by parsing an rfc3986 URI string. Returns an + // InvalidArgumentError on failure. static absl::StatusOr Parse(absl::string_view uri_text); - // Explicit construction by individual URI components - URI(std::string scheme, std::string authority, std::string path, + // Creates a URI from components. Returns an InvalidArgumentError on failure. + static absl::StatusOr Create( + std::string scheme, std::string authority, std::string path, std::vector query_parameter_pairs, std::string fragment); + URI() = default; + // Copy construction and assignment URI(const URI& other); URI& operator=(const URI& other); @@ -56,7 +62,8 @@ class URI { URI(URI&&) = default; URI& operator=(URI&&) = default; - static std::string PercentEncode(absl::string_view str); + static std::string PercentEncodePath(absl::string_view str); + static std::string PercentDecode(absl::string_view str); const std::string& scheme() const { return scheme_; } @@ -77,7 +84,12 @@ class URI { } const std::string& fragment() const { return fragment_; } + std::string ToString() const; + private: + URI(std::string scheme, std::string authority, std::string path, + std::vector query_parameter_pairs, std::string fragment); + std::string scheme_; std::string authority_; std::string path_; @@ -87,4 +99,4 @@ class URI { }; } // namespace grpc_core -#endif /* GRPC_CORE_LIB_URI_URI_PARSER_H */ +#endif // GRPC_CORE_LIB_URI_URI_PARSER_H diff --git a/test/core/uri/uri_parser_test.cc b/test/core/uri/uri_parser_test.cc index a4b1b6b8cca..987b44134a0 100644 --- a/test/core/uri/uri_parser_test.cc +++ b/test/core/uri/uri_parser_test.cc @@ -1,20 +1,18 @@ -/* - * - * Copyright 2015 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ +// +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// #include "src/core/lib/uri/uri_parser.h" @@ -34,28 +32,33 @@ using ::testing::Contains; using ::testing::ElementsAre; using ::testing::Pair; -static void TestSucceeds( - absl::string_view uri_text, absl::string_view scheme, - absl::string_view authority, absl::string_view path, - const std::map& query_param_map, - const std::vector& query_param_pairs, - absl::string_view fragment) { - absl::StatusOr uri = grpc_core::URI::Parse(uri_text); - ASSERT_TRUE(uri.ok()); - EXPECT_EQ(scheme, uri->scheme()); - EXPECT_EQ(authority, uri->authority()); - EXPECT_EQ(path, uri->path()); - EXPECT_THAT(uri->query_parameter_map(), ContainerEq(query_param_map)); - EXPECT_THAT(uri->query_parameter_pairs(), ContainerEq(query_param_pairs)); - EXPECT_EQ(fragment, uri->fragment()); -} - -static void TestFails(absl::string_view uri_text) { - absl::StatusOr uri = grpc_core::URI::Parse(uri_text); - ASSERT_FALSE(uri.ok()); -} +namespace grpc_core { + +class URIParserTest : public testing::Test { + protected: + static void TestSucceeds( + absl::string_view uri_text, absl::string_view scheme, + absl::string_view authority, absl::string_view path, + const std::map& query_param_map, + const std::vector& query_param_pairs, + absl::string_view fragment) { + absl::StatusOr uri = URI::Parse(uri_text); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(scheme, uri->scheme()); + EXPECT_EQ(authority, uri->authority()); + EXPECT_EQ(path, uri->path()); + EXPECT_THAT(uri->query_parameter_map(), ContainerEq(query_param_map)); + EXPECT_THAT(uri->query_parameter_pairs(), ContainerEq(query_param_pairs)); + EXPECT_EQ(fragment, uri->fragment()); + } -TEST(URIParserTest, BasicExamplesAreParsedCorrectly) { + static void TestFails(absl::string_view uri_text) { + absl::StatusOr uri = URI::Parse(uri_text); + ASSERT_FALSE(uri.ok()); + } +}; + +TEST_F(URIParserTest, BasicExamplesAreParsedCorrectly) { TestSucceeds("http://www.google.com", "http", "www.google.com", "", {}, {}, ""); TestSucceeds("dns:///foo", "dns", "", "/foo", {}, {}, ""); @@ -75,7 +78,7 @@ TEST(URIParserTest, BasicExamplesAreParsedCorrectly) { "buckle/my/shoe"); } -TEST(URIParserTest, UncommonValidExamplesAreParsedCorrectly) { +TEST_F(URIParserTest, UncommonValidExamplesAreParsedCorrectly) { TestSucceeds("scheme:path//is/ok", "scheme", "", "path//is/ok", {}, {}, ""); TestSucceeds("http:?legit", "http", "", "", {{"legit", ""}}, {{"legit", ""}}, ""); @@ -84,15 +87,18 @@ TEST(URIParserTest, UncommonValidExamplesAreParsedCorrectly) { TestSucceeds("http:?legit#twice", "http", "", "", {{"legit", ""}}, {{"legit", ""}}, "twice"); TestSucceeds("fake:///", "fake", "", "/", {}, {}, ""); + TestSucceeds("http://local%25host:8080/whatz%25it?1%25=2%25#fragment", "http", + "local%host:8080", "/whatz%it", {{"1%", "2%"}}, {{"1%", "2%"}}, + "fragment"); } -TEST(URIParserTest, VariousKeyValueAndNonKVQueryParamsAreParsedCorrectly) { +TEST_F(URIParserTest, VariousKeyValueAndNonKVQueryParamsAreParsedCorrectly) { TestSucceeds("http://foo/path?a&b=B&c=&#frag", "http", "foo", "/path", {{"c", ""}, {"a", ""}, {"b", "B"}}, {{"a", ""}, {"b", "B"}, {"c", ""}}, "frag"); } -TEST(URIParserTest, ParserTreatsFirstEqualSignAsKVDelimiterInQueryString) { +TEST_F(URIParserTest, ParserTreatsFirstEqualSignAsKVDelimiterInQueryString) { TestSucceeds( "http://localhost:8080/?too=many=equals&are=present=here#fragged", "http", "localhost:8080", "/", {{"are", "present=here"}, {"too", "many=equals"}}, @@ -102,52 +108,48 @@ TEST(URIParserTest, ParserTreatsFirstEqualSignAsKVDelimiterInQueryString) { {{"foo", "bar=baz"}, {"foobar", "=="}}, ""); } -TEST(URIParserTest, - RepeatedQueryParamsAreSupportedInOrderedPairsButDeduplicatedInTheMap) { - absl::StatusOr uri = - grpc_core::URI::Parse("http://foo/path?a=2&a=1&a=3"); - ASSERT_TRUE(uri.ok()); +TEST_F(URIParserTest, + RepeatedQueryParamsAreSupportedInOrderedPairsButDeduplicatedInTheMap) { + absl::StatusOr uri = URI::Parse("http://foo/path?a=2&a=1&a=3"); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); // The map stores the last found value. ASSERT_THAT(uri->query_parameter_map(), ElementsAre(Pair("a", "3"))); // Order matters for query parameter pairs ASSERT_THAT(uri->query_parameter_pairs(), - ElementsAre(grpc_core::URI::QueryParam{"a", "2"}, - grpc_core::URI::QueryParam{"a", "1"}, - grpc_core::URI::QueryParam{"a", "3"})); + ElementsAre(URI::QueryParam{"a", "2"}, URI::QueryParam{"a", "1"}, + URI::QueryParam{"a", "3"})); } -TEST(URIParserTest, QueryParamMapRemainsValiditAfterMovingTheURI) { - grpc_core::URI uri_copy; +TEST_F(URIParserTest, QueryParamMapRemainsValiditAfterMovingTheURI) { + URI uri_copy; { - absl::StatusOr uri = - grpc_core::URI::Parse("http://foo/path?a=2&b=1&c=3"); - ASSERT_TRUE(uri.ok()); + absl::StatusOr uri = URI::Parse("http://foo/path?a=2&b=1&c=3"); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); uri_copy = std::move(*uri); } // ASSERT_EQ(uri_copy.query_parameter_map().find("a")->second, "2"); ASSERT_THAT(uri_copy.query_parameter_map(), Contains(Pair("a", "2"))); } -TEST(URIParserTest, QueryParamMapRemainsValidAfterCopyingTheURI) { +TEST_F(URIParserTest, QueryParamMapRemainsValidAfterCopyingTheURI) { // Since the query parameter map points to objects stored in the param pair // vector, this test checks that the param map pointers remain valid after // a copy. Ideally {a,m}san will catch this if there's a problem. // testing copy operator=: - grpc_core::URI uri_copy; + URI uri_copy; { - absl::StatusOr del_uri = - grpc_core::URI::Parse("http://foo/path?a=2&b=1&c=3"); - ASSERT_TRUE(del_uri.ok()); + absl::StatusOr del_uri = URI::Parse("http://foo/path?a=2&b=1&c=3"); + ASSERT_TRUE(del_uri.ok()) << del_uri.status().ToString(); uri_copy = *del_uri; } ASSERT_THAT(uri_copy.query_parameter_map(), Contains(Pair("a", "2"))); - grpc_core::URI* del_uri2 = new grpc_core::URI(uri_copy); - grpc_core::URI uri_copy2(*del_uri2); + URI* del_uri2 = new URI(uri_copy); + URI uri_copy2(*del_uri2); delete del_uri2; ASSERT_THAT(uri_copy2.query_parameter_map(), Contains(Pair("a", "2"))); } -TEST(URIParserTest, AWSExternalAccountRegressionTest) { +TEST_F(URIParserTest, AWSExternalAccountRegressionTest) { TestSucceeds( "https://foo.com:5555/v1/" "token-exchange?subject_token=eyJhbGciO&subject_token_type=urn:ietf:" @@ -160,20 +162,21 @@ TEST(URIParserTest, AWSExternalAccountRegressionTest) { ""); } -TEST(URIParserTest, NonKeyValueQueryStringsWork) { +TEST_F(URIParserTest, NonKeyValueQueryStringsWork) { TestSucceeds("http://www.google.com?yay-i'm-using-queries", "http", "www.google.com", "", {{"yay-i'm-using-queries", ""}}, {{"yay-i'm-using-queries", ""}}, ""); } -TEST(URIParserTest, IPV6StringsAreParsedCorrectly) { +TEST_F(URIParserTest, IPV6StringsAreParsedCorrectly) { TestSucceeds("ipv6:[2001:db8::1%252]:12345", "ipv6", "", "[2001:db8::1%2]:12345", {}, {}, ""); TestSucceeds("ipv6:[fe80::90%eth1.sky1]:6010", "ipv6", "", "[fe80::90%eth1.sky1]:6010", {}, {}, ""); } -TEST(URIParserTest, PreviouslyReservedCharactersInUnrelatedURIPartsAreIgnored) { +TEST_F(URIParserTest, + PreviouslyReservedCharactersInUnrelatedURIPartsAreIgnored) { // The '?' and '/' characters are not reserved delimiter characters in the // fragment. See http://go/rfc/3986#section-3.5 TestSucceeds("http://foo?bar#lol?", "http", "foo", "", {{"bar", ""}}, @@ -182,30 +185,30 @@ TEST(URIParserTest, PreviouslyReservedCharactersInUnrelatedURIPartsAreIgnored) { {{"bar", ""}}, "lol?/"); } -TEST(URIParserTest, EncodedCharactersInQueryStringAreParsedCorrectly) { +TEST_F(URIParserTest, EncodedCharactersInQueryStringAreParsedCorrectly) { TestSucceeds("https://www.google.com/?a=1%26b%3D2&c=3", "https", "www.google.com", "/", {{"c", "3"}, {"a", "1&b=2"}}, {{"a", "1&b=2"}, {"c", "3"}}, ""); } -TEST(URIParserTest, InvalidPercentEncodingsArePassedThrough) { +TEST_F(URIParserTest, InvalidPercentEncodingsArePassedThrough) { TestSucceeds("x:y?%xx", "x", "", "y", {{"%xx", ""}}, {{"%xx", ""}}, ""); TestSucceeds("http:?dangling-pct-%0", "http", "", "", {{"dangling-pct-%0", ""}}, {{"dangling-pct-%0", ""}}, ""); } -TEST(URIParserTest, NullCharactersInURIStringAreSupported) { +TEST_F(URIParserTest, NullCharactersInURIStringAreSupported) { // Artificial examples to show that embedded nulls are supported. TestSucceeds(std::string("unix-abstract:\0should-be-ok", 27), "unix-abstract", "", std::string("\0should-be-ok", 13), {}, {}, ""); } -TEST(URIParserTest, EncodedNullsInURIStringAreSupported) { +TEST_F(URIParserTest, EncodedNullsInURIStringAreSupported) { TestSucceeds("unix-abstract:%00x", "unix-abstract", "", std::string("\0x", 2), {}, {}, ""); } -TEST(URIParserTest, InvalidURIsResultInFailureStatuses) { +TEST_F(URIParserTest, InvalidURIsResultInFailureStatuses) { TestFails("xyz"); TestFails("http://foo?[bar]"); TestFails("http://foo?x[bar]"); @@ -215,13 +218,253 @@ TEST(URIParserTest, InvalidURIsResultInFailureStatuses) { TestFails("0invalid_scheme:must_start/with?alpha"); } -TEST(URIParserTest, PercentEncode) { - // Ensure "?" and "=" are percent encoded; and ":" is escaped. - std::string input = "127.0.0.1:22222?psm_project_id=1234"; - EXPECT_EQ("127.0.0.1:22222%3fpsm_project_id%3d1234", - grpc_core::URI::PercentEncode(input)); +TEST(URITest, PercentEncodePath) { + EXPECT_EQ(URI::PercentEncodePath( + // These chars are allowed. + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "/:@-._~!$&'()*+,;=" + // These chars will be escaped. + "\\?%#[]^"), + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "/:@-._~!$&'()*+,;=" + "%5C%3F%25%23%5B%5D%5E"); +} + +TEST(URITest, Basic) { + auto uri = + URI::Create("http", "server.example.com", "/path/to/file.html", {}, ""); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), "server.example.com"); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT(uri->query_parameter_pairs(), testing::ElementsAre()); + EXPECT_THAT(uri->query_parameter_map(), testing::ElementsAre()); + EXPECT_EQ(uri->fragment(), ""); + EXPECT_EQ("http://server.example.com/path/to/file.html", uri->ToString()); +} + +TEST(URITest, NoAuthority) { + auto uri = URI::Create("http", "", "/path/to/file.html", {}, ""); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), ""); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT(uri->query_parameter_pairs(), testing::ElementsAre()); + EXPECT_THAT(uri->query_parameter_map(), testing::ElementsAre()); + EXPECT_EQ(uri->fragment(), ""); + EXPECT_EQ("http:/path/to/file.html", uri->ToString()); +} + +TEST(URITest, NoAuthorityRelativePath) { + auto uri = URI::Create("http", "", "path/to/file.html", {}, ""); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), ""); + EXPECT_EQ(uri->path(), "path/to/file.html"); + EXPECT_THAT(uri->query_parameter_pairs(), testing::ElementsAre()); + EXPECT_THAT(uri->query_parameter_map(), testing::ElementsAre()); + EXPECT_EQ(uri->fragment(), ""); + EXPECT_EQ("http:path/to/file.html", uri->ToString()); +} + +TEST(URITest, AuthorityRelativePath) { + auto uri = + URI::Create("http", "server.example.com", "path/to/file.html", {}, ""); + ASSERT_FALSE(uri.ok()); + EXPECT_EQ(uri.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(uri.status().message(), + "if authority is present, path must start with a '/'"); +} + +TEST(URITest, QueryParams) { + auto uri = URI::Create("http", "server.example.com", "/path/to/file.html", + {{"key", "value"}, {"key2", "value2"}}, ""); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), "server.example.com"); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT( + uri->query_parameter_pairs(), + testing::ElementsAre( + testing::AllOf(testing::Field(&URI::QueryParam::key, "key"), + testing::Field(&URI::QueryParam::value, "value")), + testing::AllOf(testing::Field(&URI::QueryParam::key, "key2"), + testing::Field(&URI::QueryParam::value, "value2")))); + EXPECT_THAT(uri->query_parameter_map(), + testing::ElementsAre(testing::Pair("key", "value"), + testing::Pair("key2", "value2"))); + EXPECT_EQ(uri->fragment(), ""); + EXPECT_EQ("http://server.example.com/path/to/file.html?key=value&key2=value2", + uri->ToString()); } +TEST(URITest, DuplicateQueryParams) { + auto uri = URI::Create( + "http", "server.example.com", "/path/to/file.html", + {{"key", "value"}, {"key2", "value2"}, {"key", "other_value"}}, ""); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), "server.example.com"); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT( + uri->query_parameter_pairs(), + testing::ElementsAre( + testing::AllOf(testing::Field(&URI::QueryParam::key, "key"), + testing::Field(&URI::QueryParam::value, "value")), + testing::AllOf(testing::Field(&URI::QueryParam::key, "key2"), + testing::Field(&URI::QueryParam::value, "value2")), + testing::AllOf( + testing::Field(&URI::QueryParam::key, "key"), + testing::Field(&URI::QueryParam::value, "other_value")))); + EXPECT_THAT(uri->query_parameter_map(), + testing::ElementsAre(testing::Pair("key", "other_value"), + testing::Pair("key2", "value2"))); + EXPECT_EQ(uri->fragment(), ""); + EXPECT_EQ( + "http://server.example.com/path/to/file.html" + "?key=value&key2=value2&key=other_value", + uri->ToString()); +} + +TEST(URITest, Fragment) { + auto uri = URI::Create("http", "server.example.com", "/path/to/file.html", {}, + "fragment"); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), "server.example.com"); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT(uri->query_parameter_pairs(), testing::ElementsAre()); + EXPECT_THAT(uri->query_parameter_map(), testing::ElementsAre()); + EXPECT_EQ(uri->fragment(), "fragment"); + EXPECT_EQ("http://server.example.com/path/to/file.html#fragment", + uri->ToString()); +} + +TEST(URITest, QueryParamsAndFragment) { + auto uri = URI::Create("http", "server.example.com", "/path/to/file.html", + {{"key", "value"}, {"key2", "value2"}}, "fragment"); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), "http"); + EXPECT_EQ(uri->authority(), "server.example.com"); + EXPECT_EQ(uri->path(), "/path/to/file.html"); + EXPECT_THAT( + uri->query_parameter_pairs(), + testing::ElementsAre( + testing::AllOf(testing::Field(&URI::QueryParam::key, "key"), + testing::Field(&URI::QueryParam::value, "value")), + testing::AllOf(testing::Field(&URI::QueryParam::key, "key2"), + testing::Field(&URI::QueryParam::value, "value2")))); + EXPECT_THAT(uri->query_parameter_map(), + testing::ElementsAre(testing::Pair("key", "value"), + testing::Pair("key2", "value2"))); + EXPECT_EQ(uri->fragment(), "fragment"); + EXPECT_EQ( + "http://server.example.com/path/to/" + "file.html?key=value&key2=value2#fragment", + uri->ToString()); +} + +TEST(URITest, ToStringPercentEncoding) { + auto uri = URI::Create( + // Scheme allowed chars. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-." + // Scheme escaped chars. + "%:/?#[]@!$&'()*,;=", + // Authority allowed chars. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-.+~!$&'()*+,;=:[]@" + // Authority escaped chars. + "%/?#", + // Path allowed chars. + "/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$&'()*+,;=:@" + // Path escaped chars. + "%?#[]", + {{// Query allowed chars. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + // Query escaped chars. + "%=&#[]", + // Query allowed chars. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + // Query escaped chars. + "%=&#[]"}}, + // Fragment allowed chars. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?=&" + // Fragment escaped chars. + "%#[]"); + ASSERT_TRUE(uri.ok()) << uri.status().ToString(); + EXPECT_EQ(uri->scheme(), + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-." + "%:/?#[]@!$&'()*,;="); + EXPECT_EQ(uri->authority(), + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-.+~!$&'()*+,;=:[]@" + "%/?#"); + EXPECT_EQ(uri->path(), + "/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$&'()*+,;=:@" + "%?#[]"); + EXPECT_THAT( + uri->query_parameter_pairs(), + testing::ElementsAre(testing::AllOf( + testing::Field( + &URI::QueryParam::key, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%=&#[]"), + testing::Field( + &URI::QueryParam::value, + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%=&#[]")))); + EXPECT_THAT( + uri->query_parameter_map(), + testing::ElementsAre(testing::Pair( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%=&#[]", + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%=&#[]"))); + EXPECT_EQ(uri->fragment(), + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?=&" + "%#[]"); + EXPECT_EQ( + // Scheme + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-." + "%25%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2C%3B%3D" + // Authority + "://abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-.+~!$&'()*+,;=:[]@" + "%25%2F%3F%23" + // Path + "/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$&'()*+,;=:@" + "%25%3F%23%5B%5D" + // Query + "?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%25%3D%26%23%5B%5D" + "=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?" + "%25%3D%26%23%5B%5D" + // Fragment + "#abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "-._~!$'()*+,;:@/?=&" + "%25%23%5B%5D", + uri->ToString()); +} + +} // namespace grpc_core + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); grpc::testing::TestEnvironment env(argc, argv);