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
pull/28515/head
Mark D. Roth 3 years ago committed by GitHub
parent 97b87da470
commit 90340c24c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc
  2. 14
      src/core/ext/filters/client_channel/resolver/xds/xds_resolver.cc
  3. 278
      src/core/lib/uri/uri_parser.cc
  4. 58
      src/core/lib/uri/uri_parser.h
  5. 393
      test/core/uri/uri_parser_test.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;
}

@ -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}});

@ -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 <grpc/support/port_platform.h>
@ -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<bool(char)> 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> URI::Parse(absl::string_view uri_text) {
absl::StatusOr<std::string> 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> 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<QueryParam> 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> 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> URI::Parse(absl::string_view uri_text) {
std::move(query_param_pairs), std::move(fragment));
}
absl::StatusOr<URI> URI::Create(std::string scheme, std::string authority,
std::string path,
std::vector<QueryParam> 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<QueryParam> 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<std::string> 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

@ -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<URI> 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<URI> Create(
std::string scheme, std::string authority, std::string path,
std::vector<QueryParam> 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<QueryParam> 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

@ -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<absl::string_view, absl::string_view>& query_param_map,
const std::vector<grpc_core::URI::QueryParam>& query_param_pairs,
absl::string_view fragment) {
absl::StatusOr<grpc_core::URI> 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<grpc_core::URI> 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<absl::string_view, absl::string_view>& query_param_map,
const std::vector<URI::QueryParam>& query_param_pairs,
absl::string_view fragment) {
absl::StatusOr<URI> 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 = 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<grpc_core::URI> 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 = 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<grpc_core::URI> uri =
grpc_core::URI::Parse("http://foo/path?a=2&b=1&c=3");
ASSERT_TRUE(uri.ok());
absl::StatusOr<URI> 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<grpc_core::URI> del_uri =
grpc_core::URI::Parse("http://foo/path?a=2&b=1&c=3");
ASSERT_TRUE(del_uri.ok());
absl::StatusOr<URI> 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);

Loading…
Cancel
Save