From ed28d142ee2195240c44cef9f7931954409eb52f Mon Sep 17 00:00:00 2001 From: htuch Date: Thu, 25 Jun 2020 15:24:14 -0400 Subject: [PATCH] udpa: UDPA URI encoding/decoding utils. (#11678) These map between the structured udpa::core::v1::ResourceName message and flat udpa:// URI representations of resource names. Risk level: Low Testing: Unit tests added. Part of #11264. Signed-off-by: Harvey Tuch --- api/bazel/repository_locations.bzl | 4 +- .../bazel/repository_locations.bzl | 4 +- source/common/config/BUILD | 10 +++ source/common/config/udpa_resource.cc | 79 +++++++++++++++++ source/common/config/udpa_resource.h | 48 +++++++++++ test/common/config/BUILD | 9 ++ test/common/config/udpa_resource_test.cc | 84 +++++++++++++++++++ tools/spelling/spelling_dictionary.txt | 1 + 8 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 source/common/config/udpa_resource.cc create mode 100644 source/common/config/udpa_resource.h create mode 100644 test/common/config/udpa_resource_test.cc diff --git a/api/bazel/repository_locations.bzl b/api/bazel/repository_locations.bzl index afe78af1e47b..80989a6f4d35 100644 --- a/api/bazel/repository_locations.bzl +++ b/api/bazel/repository_locations.bzl @@ -13,8 +13,8 @@ GOOGLEAPIS_SHA = "a45019af4d3290f02eaeb1ce10990166978c807cb33a9692141a076ba46d14 PROMETHEUS_GIT_SHA = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" # Nov 17, 2017 PROMETHEUS_SHA = "783bdaf8ee0464b35ec0c8704871e1e72afa0005c3f3587f65d9d6694bf3911b" -UDPA_GIT_SHA = "9f54a527e3bf4d1f4a6527f93d329fb1cc4516ac" # May 8, 2020 -UDPA_SHA256 = "7edae88586a84360203e5a4c724080c740b7b6002d5d56f5e806f27c912895cd" +UDPA_GIT_SHA = "ca580c4fcf87b178547c2e9e41a2481b0008efe9" # June 24, 2020 +UDPA_SHA256 = "a1dc305cd56f1dd393fec8ec6b19f4f7d76af9740c7746e9377c8dd480f77e70" ZIPKINAPI_RELEASE = "0.2.2" # Aug 23, 2019 ZIPKINAPI_SHA256 = "688c4fe170821dd589f36ec45aaadc03a618a40283bc1f97da8fa11686fc816b" diff --git a/generated_api_shadow/bazel/repository_locations.bzl b/generated_api_shadow/bazel/repository_locations.bzl index afe78af1e47b..80989a6f4d35 100644 --- a/generated_api_shadow/bazel/repository_locations.bzl +++ b/generated_api_shadow/bazel/repository_locations.bzl @@ -13,8 +13,8 @@ GOOGLEAPIS_SHA = "a45019af4d3290f02eaeb1ce10990166978c807cb33a9692141a076ba46d14 PROMETHEUS_GIT_SHA = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" # Nov 17, 2017 PROMETHEUS_SHA = "783bdaf8ee0464b35ec0c8704871e1e72afa0005c3f3587f65d9d6694bf3911b" -UDPA_GIT_SHA = "9f54a527e3bf4d1f4a6527f93d329fb1cc4516ac" # May 8, 2020 -UDPA_SHA256 = "7edae88586a84360203e5a4c724080c740b7b6002d5d56f5e806f27c912895cd" +UDPA_GIT_SHA = "ca580c4fcf87b178547c2e9e41a2481b0008efe9" # June 24, 2020 +UDPA_SHA256 = "a1dc305cd56f1dd393fec8ec6b19f4f7d76af9740c7746e9377c8dd480f77e70" ZIPKINAPI_RELEASE = "0.2.2" # Aug 23, 2019 ZIPKINAPI_SHA256 = "688c4fe170821dd589f36ec45aaadc03a618a40283bc1f97da8fa11686fc816b" diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 19a26940fbb8..4a57dc587013 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -300,6 +300,16 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "udpa_resource_lib", + srcs = ["udpa_resource.cc"], + hdrs = ["udpa_resource.h"], + deps = [ + "//source/common/http:utility_lib", + "@com_github_cncf_udpa//udpa/core/v1:pkg_cc_proto", + ], +) + envoy_cc_library( name = "update_ack_lib", hdrs = ["update_ack.h"], diff --git a/source/common/config/udpa_resource.cc b/source/common/config/udpa_resource.cc new file mode 100644 index 000000000000..6c10d4cc5e3f --- /dev/null +++ b/source/common/config/udpa_resource.cc @@ -0,0 +1,79 @@ +#include "common/config/udpa_resource.h" + +#include + +#include "common/common/fmt.h" +#include "common/http/utility.h" + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" + +// TODO(htuch): This file has a bunch of ad hoc URI encoding/decoding based on Envoy's HTTP util +// functions. Once https://github.com/envoyproxy/envoy/issues/6588 lands, we can replace with GURL. + +namespace Envoy { +namespace Config { + +using PercentEncoding = Http::Utility::PercentEncoding; + +std::string UdpaResourceName::encodeUri(const udpa::core::v1::ResourceName& resource_name, + const EncodeOptions& options) { + // We need to percent-encode authority, id, path and query params. Qualified types should not have + // reserved characters. + const std::string authority = PercentEncoding::encode(resource_name.authority(), "%/?#"); + std::vector path_components; + for (const auto& id_component : resource_name.id()) { + path_components.emplace_back(PercentEncoding::encode(id_component, "%:/?#[]")); + } + const std::string path = absl::StrJoin(path_components, "/"); + std::vector query_param_components; + for (const auto& context_param : resource_name.context().params()) { + query_param_components.emplace_back( + absl::StrCat(PercentEncoding::encode(context_param.first, "%#[]&="), "=", + PercentEncoding::encode(context_param.second, "%#[]&="))); + } + if (options.sort_context_params_) { + std::sort(query_param_components.begin(), query_param_components.end()); + } + const std::string query_params = + query_param_components.empty() ? "" : "?" + absl::StrJoin(query_param_components, "&"); + return absl::StrCat("udpa://", authority, "/", resource_name.resource_type(), + path.empty() ? "" : "/", path, query_params); +} + +udpa::core::v1::ResourceName UdpaResourceName::decodeUri(absl::string_view resource_uri) { + if (!absl::StartsWith(resource_uri, "udpa:")) { + throw UdpaResourceName::DecodeException( + fmt::format("{} does not have an udpa scheme", resource_uri)); + } + absl::string_view host, path; + Http::Utility::extractHostPathFromUri(resource_uri, host, path); + udpa::core::v1::ResourceName decoded_resource_name; + decoded_resource_name.set_authority(PercentEncoding::decode(host)); + const size_t query_params_start = path.find('?'); + Http::Utility::QueryParams query_params; + if (query_params_start != absl::string_view::npos) { + query_params = Http::Utility::parseQueryString(path.substr(query_params_start)); + for (const auto& it : query_params) { + (*decoded_resource_name.mutable_context() + ->mutable_params())[PercentEncoding::decode(it.first)] = + PercentEncoding::decode(it.second); + } + path = path.substr(0, query_params_start); + } + // This is guaranteed by Http::Utility::extractHostPathFromUri. + ASSERT(absl::StartsWith(path, "/")); + const std::vector path_components = absl::StrSplit(path.substr(1), '/'); + decoded_resource_name.set_resource_type(std::string(path_components[0])); + if (decoded_resource_name.resource_type().empty()) { + throw UdpaResourceName::DecodeException( + fmt::format("Qualified type missing from {}", resource_uri)); + } + for (auto it = std::next(path_components.cbegin()); it != path_components.cend(); it++) { + decoded_resource_name.add_id(PercentEncoding::decode(*it)); + } + return decoded_resource_name; +} + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/udpa_resource.h b/source/common/config/udpa_resource.h new file mode 100644 index 000000000000..8c81eab9143b --- /dev/null +++ b/source/common/config/udpa_resource.h @@ -0,0 +1,48 @@ +#include "envoy/common/exception.h" + +#include "absl/strings/string_view.h" +#include "udpa/core/v1/resource_name.pb.h" + +namespace Envoy { +namespace Config { + +// Utilities for URI encoding/decoding of udpa::core::v1::ResourceName. +class UdpaResourceName { +public: + // Options for encoded URIs. + struct EncodeOptions { + // Should the context params be sorted by key? This provides deterministic encoding. + bool sort_context_params_{}; + }; + + /** + * Encode a udpa::core::v1::ResourceName message as a udpa:// URI string. + * + * @param resource_name resource name message. + * @param options encoding options. + * @return std::string udpa:// URI for resource_name. + */ + static std::string encodeUri(const udpa::core::v1::ResourceName& resource_name, + const EncodeOptions& options); + static std::string encodeUri(const udpa::core::v1::ResourceName& resource_name) { + return encodeUri(resource_name, {}); + } + + // Thrown when an exception occurs during URI decoding. + class DecodeException : public EnvoyException { + public: + DecodeException(const std::string& what) : EnvoyException(what) {} + }; + + /** + * Decode a udpa:// URI string to a udpa::core::v1::ResourceName. + * + * @param resource_uri udpa:// resource URI. + * @return udpa::core::v1::ResourceName resource name message for resource_uri. + * @throws DecodeException when parsing fails. + */ + static udpa::core::v1::ResourceName decodeUri(absl::string_view resource_uri); +}; + +} // namespace Config +} // namespace Envoy diff --git a/test/common/config/BUILD b/test/common/config/BUILD index 4d55e78e637e..06cda3b3dfd6 100644 --- a/test/common/config/BUILD +++ b/test/common/config/BUILD @@ -416,6 +416,15 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "udpa_resource_test", + srcs = ["udpa_resource_test.cc"], + deps = [ + "//source/common/config:udpa_resource_lib", + "//test/test_common:utility_lib", + ], +) + envoy_proto_library( name = "version_converter_proto", srcs = ["version_converter.proto"], diff --git a/test/common/config/udpa_resource_test.cc b/test/common/config/udpa_resource_test.cc new file mode 100644 index 000000000000..ad05b37ed8e5 --- /dev/null +++ b/test/common/config/udpa_resource_test.cc @@ -0,0 +1,84 @@ +#include "common/config/udpa_resource.h" + +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Config { +namespace { + +const std::string EscapedUri = + "udpa://f123%25%2F%3F%23o/envoy.config.listener.v3.Listener/b%25%3A%2F%3F%23%5B%5Dar//" + "baz?%25%23%5B%5D%26%3Dab=cde%25%23%5B%5D%26%3Df"; +const std::string EscapedUriWithManyQueryParams = + "udpa://f123%25%2F%3F%23o/envoy.config.listener.v3.Listener/b%25%3A%2F%3F%23%5B%5Dar//" + "baz?%25%23%5B%5D%26%3D=bar&%25%23%5B%5D%26%3Dab=cde%25%23%5B%5D%26%3Df&foo=%25%23%5B%5D%26%3D"; + +// for all x. encodeUri(decodeUri(x)) = x where x comes from sample of valid udpa:// URIs. +// TODO(htuch): write a fuzzer that validates this property as well. +TEST(UdpaResourceNameTest, DecodeEncode) { + const std::vector uris = { + "udpa:///envoy.config.listener.v3.Listener", + "udpa://foo/envoy.config.listener.v3.Listener", + "udpa://foo/envoy.config.listener.v3.Listener/bar", + "udpa://foo/envoy.config.listener.v3.Listener/bar/baz", + "udpa://foo/envoy.config.listener.v3.Listener/bar////baz", + "udpa://foo/envoy.config.listener.v3.Listener?ab=cde", + "udpa://foo/envoy.config.listener.v3.Listener/bar?ab=cd", + "udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=cde", + "udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=", + "udpa://foo/envoy.config.listener.v3.Listener/bar/baz?=cd", + "udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=cde&ba=edc&z=f", + EscapedUri, + EscapedUriWithManyQueryParams, + }; + UdpaResourceName::EncodeOptions encode_options; + encode_options.sort_context_params_ = true; + for (const std::string& uri : uris) { + EXPECT_EQ(uri, UdpaResourceName::encodeUri(UdpaResourceName::decodeUri(uri), encode_options)); + } +} + +// Validate that URI decoding behaves as expected component-wise. +TEST(UdpaResourceNameTest, DecodeSuccess) { + const auto resource_name = UdpaResourceName::decodeUri(EscapedUriWithManyQueryParams); + EXPECT_EQ("f123%/?#o", resource_name.authority()); + EXPECT_EQ("envoy.config.listener.v3.Listener", resource_name.resource_type()); + EXPECT_EQ(3, resource_name.id().size()); + EXPECT_EQ("b%:/?#[]ar", resource_name.id()[0]); + EXPECT_EQ("", resource_name.id()[1]); + EXPECT_EQ("baz", resource_name.id()[2]); + EXPECT_EQ(3, resource_name.context().params().size()); + EXPECT_EQ("bar", resource_name.context().params().at("%#[]&=")); + EXPECT_EQ("cde%#[]&=f", resource_name.context().params().at("%#[]&=ab")); + EXPECT_EQ("%#[]&=", resource_name.context().params().at("foo")); +} + +// Validate that the URI decoding behaves with a near-empty UDPA resource name. +TEST(UdpaResourceNameTest, DecodeEmpty) { + const auto resource_name = + UdpaResourceName::decodeUri("udpa:///envoy.config.listener.v3.Listener"); + EXPECT_TRUE(resource_name.authority().empty()); + EXPECT_EQ("envoy.config.listener.v3.Listener", resource_name.resource_type()); + EXPECT_TRUE(resource_name.id().empty()); + EXPECT_TRUE(resource_name.context().params().empty()); +} + +// Negative tests for URI decoding. +TEST(UdpaResourceNameTest, DecodeFail) { + { + EXPECT_THROW_WITH_MESSAGE(UdpaResourceName::decodeUri("foo://"), + UdpaResourceName::DecodeException, + "foo:// does not have an udpa scheme"); + } + { + EXPECT_THROW_WITH_MESSAGE(UdpaResourceName::decodeUri("udpa://foo"), + UdpaResourceName::DecodeException, + "Qualified type missing from udpa://foo"); + } +} + +} // namespace +} // namespace Config +} // namespace Envoy diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index a4483164e7e2..9bfe64a17c0d 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -1105,6 +1105,7 @@ typedef typeid typesafe ucontext +udpa uint un- unacked