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..40e5133597ba --- /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.qualified_type(), + path.empty() && query_params.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 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_qualified_type(std::string(path_components[0])); + if (decoded_resource_name.qualified_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..eec748eda5bb --- /dev/null +++ b/test/common/config/udpa_resource_test.cc @@ -0,0 +1,72 @@ +#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%23%3F%26%3Do/envoy.config.listener.v3.Listener/b%25%2F%23%3F%26%3Dar//" + "baz?%25%23%26%3Dab=cde%25%23%26%3Df"; +const std::string EscapedUriWithManyQueryParams = + "udpa://f123%25%2F%23%3F%26%3Do/envoy.config.listener.v3.Listener/b%25%2F%23%3F%26%3Dar//" + "baz?%25%23%26%3D=bar&%25%23%26%3Dab=cde%25%23%26%3Df&foo=%25%23%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://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/?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.qualified_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")); +} + +// Negative tests for URI decoding. +TEST(UdpaResourceNameTest, DecodeFail) { + { + EXPECT_THROW_WITH_MESSAGE(UdpaResourceName::decodeUri("foo://"), + UdpaResourceName::DecodeException, + "foo:// does not have 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 a9c2b9af21ef..bf58fd053434 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -1102,6 +1102,7 @@ typedef typeid typesafe ucontext +udpa uint un- unacked